Перевод требует одной транзакции, защиты от конкурентного изменения обоих счетов и проверок суммы. Одна аннотация транзакции не устраняет lost update в схеме «прочитал — вычислил — записал».
Ревизия и улучшение функции денежного перевода на Kotlin
Перевод требует одной транзакции, защиты от конкурентного изменения обоих счетов и проверок суммы. Одна аннотация транзакции не устраняет lost update в схеме «прочитал — вычислил — записал».
Короткий ответ
Что ответить на собеседовании
Подробный разбор
Ответ с пояснениями
Условие
В данном примере показана простая функция на Kotlin, осуществляющая перевод средств с одного счета на другой. Необходимо выявить потенциальные проблемы, связанные с транзакциями, и предложить способы её оптимизации или полной замены.
fun transfer(sourceAccountId: Long, destinationAccountId: Long, transferAmount: Int) {
val sourceCurrentBalance = dao.getCurrentSum(sourceAccountId)
dao.updateSum(sourceAccountId, sourceCurrentBalance - transferAmount)
val destinationCurrentBalance = dao.getCurrentSum(destinationAccountId)
dao.updateSum(destinationAccountId, destinationCurrentBalance + transferAmount)
}
Что может пойти не так?
- Отсутствие проверки достаточности средств на исходном счёте → возможен отрицательный баланс.
- Нет валидации входных параметров (отрицательная сумма, одинаковые идентификаторы).
- Операции чтения и записи не находятся в одной атомарной транзакции → при конкурентных запросах может возникнуть состояние гонки и потеря средств.
- Ошибки доступа к базе (исключения) не обрабатываются, что может привести к частичному выполнению операции.
- Нет логирования и аудита действий, что усложняет отладку и мониторинг.
Варианты рефакторинга:
- Обернуть чтение и обновление балансов в единую транзакцию уровня
SERIALIZABLEили использовать механизмoptimistic locking. - Добавить предварительные проверки: достаточность средств, корректность суммы, различие счетов.
- Ввести обработку исключений и откат транзакции в случае ошибки.
- Переписать функцию так, чтобы она возвращала результат операции (
Result/Either) и/или бросала специализированные исключения. - При необходимости вынести бизнес‑логику в отдельный сервис‑класс, а работу с базой оставить в DAO, что упростит тестирование (мок‑объекты).
- Добавить логирование операции и запись в аудиторскую таблицу.
Можно ли обойтись без этой функции? Если в системе уже реализован общий механизм финансовых операций (например, сервис платежей), то данную функцию следует удалить и использовать централизованный API, где уже реализованы все перечисленные проверки и транзакционность.
Ответ
Перевод требует одной транзакции, защиты от конкурентного изменения обоих счетов и проверок суммы. Одна аннотация транзакции не устраняет lost update в схеме «прочитал — вычислил — записал».
Практический вариант: проверить положительную сумму и разные счета, заблокировать обе строки в порядке их ID, проверить наличие счетов и достаточность средств, обновить оба баланса, записать операцию и зафиксировать транзакцию. Фиксированный порядок уменьшает риск deadlock. Деньги хранить целыми минимальными единицами с контролем переполнения либо подходящим decimal. Уникальный ключ операции предотвращает повторный перевод при retry. Ошибку нельзя проглатывать: нужен rollback; внешние уведомления отправляются через outbox после commit.