Напишите SQL-запрос для перевода суммы с одного аккаунта на другой с учетом блокировок и транзакций
Что проверяют
Понимаете ли вы, что перевод денег это не два UPDATE подряд, а одна транзакция с явным порядком блокировок. Заодно смотрят, как это ложится на Spring: знаете ли, когда @Transactional реально открывает транзакцию, а когда молча ничего не делает.
Как отвечать
Проговорите три вещи: атомарность, блокировка обеих строк сразу и детерминированный порядок.
BEGIN;
SELECT id, balance FROM accounts
WHERE id IN (:from_id, :to_id)
ORDER BY id
FOR UPDATE;
UPDATE accounts SET balance = balance - :amount
WHERE id = :from_id AND balance >= :amount;
-- обновлено 0 строк: средств не хватило, приложение бросает исключение
UPDATE accounts SET balance = balance + :amount
WHERE id = :to_id;
COMMIT;Условие balance >= :amount прямо в UPDATE важно: оно делает проверку и списание одной операцией, и по числу обновлённых строк видно результат. ORDER BY id в блокирующем запросе снимает дедлок на встречных переводах. Про Java добавьте: метод сервиса помечен @Transactional, откат по умолчанию только на unchecked-исключениях, в JPA пессимистичная блокировка это LockModeType.PESSIMISTIC_WRITE, а списание и зачисление в разных методах с REQUIRES_NEW разрывают атомарность.
Где спотыкаются
Пишут BEGIN вместе с IF ... THEN и RAISE EXCEPTION. Это процедурный код, в обычном SQL-скрипте он не выполнится, а внутри функции нельзя вызывать COMMIT и ROLLBACK. Либо процедура, либо управление транзакцией на стороне приложения.
Блокируют строки в порядке «сначала отправитель, потом получатель». Два встречных перевода берут одни и те же строки в обратном порядке и получают взаимную блокировку, которую база разрубит, откатив одну из транзакций.
Проверяют баланс отдельным SELECT без блокировки. На уровне изоляции READ COMMITTED между чтением и записью значение успевает измениться, и счёт уходит в минус. Ещё забывают, что вызов транзакционного метода изнутри того же бина идёт мимо прокси, и транзакции там нет вовсе.