Какие проблемы могут возникнуть при одновременном начислении и списании средств на счет сотрудника
Что проверяют
Видите ли вы классический lost update и умеете ли решать его на уровне базы, а не synchronized в сервисе. Следом обычно спрашивают про перевод между двумя счетами и про повторную отправку запроса.
Как отвечать
Опишите сценарий: два потока читают баланс 1000, один прибавляет 500, другой списывает 300, оба записывают результат. Итог 1500 или 700 вместо 1200, одна операция потеряна. Дальше варианты решения:
- атомарный UPDATE:
UPDATE account SET balance = balance - :sum WHERE id = :id AND balance >= :sumс проверкой числа изменённых строк. Самый дешёвый путь и сразу защита от ухода в минус; - пессимистическая блокировка:
SELECT ... FOR UPDATE, в JPA@Lock(LockModeType.PESSIMISTIC_WRITE). Надёжно, но операции по одному счёту встают в очередь; - оптимистическая: поле с
@Version, при конфликтеOptimisticLockExceptionи повтор. Хорошо, когда конфликты редкие; - уровень изоляции: в PostgreSQL по умолчанию Read Committed, и схема «прочитал в Java, посчитал, записал» теряет обновления даже внутри
@Transactional. На Repeatable Read и Serializable база отменит одну из транзакций ошибкой сериализации, и её нужно повторить.
Для денег есть ещё обязательные пункты. Суммы в BigDecimal и numeric, не в double. Идемпотентность: у операции есть ключ, и уникальный индекс не даёт провести повтор запроса дважды. Журнал проводок: баланс можно пересчитать и объяснить по истории операций. При переводе между счетами блокируйте их всегда в одном порядке, например по возрастанию id, иначе встречные переводы дадут deadlock.
Где спотыкаются
Предлагают synchronized или ReentrantLock в сервисе. При двух экземплярах приложения это не защищает вообще.
Считают, что @Transactional сам решает гонку. Он даёт атомарность, но не блокирует прочитанные строки.
Забывают про повторы: клиент не дождался ответа, отправил запрос снова, и зарплату начислили дважды.