Синхронизировать текущие транзакции по критериям в Spring - PullRequest
3 голосов
/ 07 марта 2019

Нужен совет Гуру.

Наша система проверяет, превышает ли total Debt amount текущий клиент допустимое значение Credit amount и, если true, добавляет новую Debt запись

if (additionalDebtAllowed(clientId, amount)) {
    deptRepository.saveAndFlush(new Debt(clientId, amount));
}

В additionalDebtAllowed() мы получаем все строки активного долга по идентификатору клиента и сравниваем с кредитным лимитом, который мы получаем из другой системы.

Проблема в том, что вызовы REST могут быть параллельными, и мы можем выполнить их в следующей ситуации:

  1. Текущий долг клиента составляет 50, его кредитный лимит составляет 100, и он просит еще 50.
  2. Обе темы получают текущий долг (50).
  3. Обе темы проверяют наличие кредитного лимита (50 + 50 <= 100) </li>
  4. Обе темы создают новые строки задолженности
  5. Теперь долг клиента составляет 150, что больше кредитного лимита.

Самый простой способ - перед чтением и сохранением данных попытаться заблокировать строку в базе данных по идентификатору клиента. Если успех - продолжай и разблокируй. Если не получится - повторите попытку. Но я предполагаю, что могут быть более красивые способы.

Мысль о SERIALIZABLE Isolation Level, но он заблокирует всю таблицу, а мне нужны синхронизации только для каждого клиента.

1 Ответ

5 голосов
/ 12 апреля 2019

Я постараюсь сделать это простым способом вместо усложнения вещей.

Я сосредоточусь на реальной проблеме, а не на красоте кода.

Мой подход, который я протестировал, был бы следующим:

Я создал основной класс, в котором два CompletableFuture имитируют два одновременных вызова для одного и того же clientId.

//Simulate lines of db debts per user
static List<Debt> debts = new ArrayList<>();

static Map<String, Object> locks = new HashMap<String, Object>();

public static void main(String[] args) {

    String clientId = "1";

    //Simulate previous insert line in db per clientId
    debts.add(new Debt(clientId,50));

    //In a operation, put in a map the clientId to lock this id
    locks.put(clientId, new Object());

    final ExecutorService executorService = Executors.newFixedThreadPool(10);

    CompletableFuture.runAsync(() -> {
        try {
            operation(clientId, 50);
        } catch (Exception e) {
        }
    }, executorService);

    CompletableFuture.runAsync(() -> {
        try {
            operation(clientId, 50);
        } catch (Exception e) {
        }
    }, executorService);

    executorService.shutdown();
}

Метод операции является ключевым. Я синхронизировал карту по clientId , что означает, что для другого clientId он не будет заблокирован, для каждого clientId он пропустит поток одновременно.

private static void operation(String clientId, Integer amount) {
    System.out.println("Entra en operacion");
    synchronized(locks.get(clientId)) {
        if(additionalDebtAllowed(clientId, 50)) {
            insertDebt(clientId, 50);
        }
    }
}

Следующие методы имитируют вставки, поиск в БД и удаленный поиск, но я думаю, что концепция понятна, я мог бы сделать это с репозиториями, но это не главное.

private static boolean additionalDebtAllowed(String clientId, Integer amount) {

    List<Debt> debts = debtsPerClient(clientId);

    int sumDebts = debts.stream().mapToInt(d -> d.getAmount()).sum();

    int limit = limitDebtPerClient(clientId);

    if(sumDebts + amount <= limit) {
        System.out.println("Debt accepted");
        return true;
    }

    System.out.println("Debt denied");

    return false;
}

//Simulate insert in db
private static void insertDebt(String clientId, Integer amount) {
    debts.add(new Debt(clientId, amount));
}

//Simulate search in db
private static List<Debt> debtsPerClient(String clientId) {

    return debts;
}

//Simulate rest petition limit debt
private static Integer limitDebtPerClient(String clientId) {

    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    return 100;
}

Вы можете протестировать его с другим clientId и другим CompletableFuture, и вы увидите, что он работает для каждого клиента в отдельности правильным образом.

Надеюсь, это поможет вам.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...