Sobes.tech
Назад к вопросам
Senior
4

Реализовать перевод средств между двумя счетами. Учитываем, что это production решение Входные параметры: @param fromAccountId уникальный идентификатор пользователя который переводит средства @param toAccountId уникальный идентификатор пользователя кому переводят средства @param amount сумма перевода. Положительное число public void transferMoney(Long fromAccountId, Long toAccountId, BigDecimal amount) { if (amount.compareTo(BigDecimal.ZERO) <= 0) { throw new RuntimeException("Amount must be > 0"); } if (fromAccountId.equals(toAccountId)) { throw new RuntimeException("Account can't be the same"); } Long firstId = Math.min(fromAccountId, toAccountId); Long secondId = Math.max(fromAccountId, toAccountId); Account first = accountRepository.findByIdForUpdate(firstId) .orElseThrow(() -> new RuntimeException("Account not found")); Account second = accountRepository.findByIdForUpdate(secondId) .orElseThrow(() -> new RuntimeException("Account not found")); Account from = fromAccountId.equals(firstId) ? first : second; Account to = fromAccountId.equals(firstId) ? second : first; if (from.getBalance().compareTo(amount) < 0) { throw new RuntimeException("Balance is Less 0"); } from.setBalance(from.getBalance().subtract(amount)); to.setBalance(from.getBalance().add(amount)); accountRepository.save(to); accountRepository.save(from); }

Компании, где спрашивали
ОТП БанкОТП Банк

Ответ от нейросети

sobes.tech AI

В представленном коде есть несколько важных моментов, но также и критическая ошибка, которая приведёт к неправильному обновлению баланса.

  1. Проверка входных параметров и логики:
  • Проверяется, что сумма перевода положительна.
  • Проверяется, что счета разные.
  • Для предотвращения дедлоков блокируем счета в порядке возрастания ID.
  1. Ошибка в обновлении балансов:
from.setBalance(from.getBalance().subtract(amount));
to.setBalance(from.getBalance().add(amount));

Здесь после вычитания суммы из from баланс from.getBalance() уже уменьшен, и при добавлении к to используется уже уменьшенный баланс from, что неверно. Нужно использовать старый баланс to.

Правильный вариант:

from.setBalance(from.getBalance().subtract(amount));
to.setBalance(to.getBalance().add(amount));
  1. Транзакционность:

Для production решения обязательно обернуть операцию в транзакцию, чтобы обеспечить атомарность и консистентность данных.

  1. Исключения:

Лучше использовать специализированные исключения, а не RuntimeException.

  1. Пример исправленного кода с транзакцией (например, с Spring):
@Transactional
public void transferMoney(Long fromAccountId, Long toAccountId, BigDecimal amount) {
    if (amount.compareTo(BigDecimal.ZERO) <= 0) {
        throw new IllegalArgumentException("Amount must be > 0");
    }
    if (fromAccountId.equals(toAccountId)) {
        throw new IllegalArgumentException("Account can't be the same");
    }

    Long firstId = Math.min(fromAccountId, toAccountId);
    Long secondId = Math.max(fromAccountId, toAccountId);

    Account first = accountRepository.findByIdForUpdate(firstId)
        .orElseThrow(() -> new EntityNotFoundException("Account not found"));
    Account second = accountRepository.findByIdForUpdate(secondId)
        .orElseThrow(() -> new EntityNotFoundException("Account not found"));

    Account from = fromAccountId.equals(firstId) ? first : second;
    Account to = fromAccountId.equals(firstId) ? second : first;

    if (from.getBalance().compareTo(amount) < 0) {
        throw new IllegalStateException("Insufficient funds");
    }

    from.setBalance(from.getBalance().subtract(amount));
    to.setBalance(to.getBalance().add(amount));

    accountRepository.save(from);
    accountRepository.save(to);
}

Таким образом, ключевые моменты — правильное обновление балансов, блокировка записей для предотвращения гонок, транзакционность и корректная обработка ошибок.