Какие варианты решения проблемы параллельного обновления одной записи в базе данных?
Что проверяют
Понимаете ли вы проблему потерянного обновления и знаете ли, что транзакция сама по себе от неё не спасает. На уровне Read Committed, а он по умолчанию в PostgreSQL и Oracle, два потока прочитают одно значение и запишут поверх друг друга. Следом просят выбрать подход под сценарий: остаток на складе, карточка, которую правят два менеджера, списание с баланса.
Как отвечать
От дешёвого варианта к тяжёлому:
- атомарный
UPDATE:UPDATE accounts SET balance = balance - 100 WHERE id = ? AND balance >= 100. Конфликт разруливает база, приложение смотрит на число изменённых строк. Работает там, где новое значение выражается через старое; - оптимистическая блокировка: колонка версии и
UPDATE ... WHERE id = ? AND version = ?. Ноль строк значит, что вас опередили. В JPA это@VersionиOptimisticLockException, обязательно с повтором операции. Выбор по умолчанию, когда конфликты редки; - пессимистическая блокировка:
SELECT ... FOR UPDATE, в JPALockModeType.PESSIMISTIC_WRITE. Строка держится до конца транзакции. Нужныlock_timeoutи единый порядок захвата строк, иначе дедлоки; - уровень изоляции:
REPEATABLE READилиSERIALIZABLE. В PostgreSQL это не блокировки, а ошибка сериализации40001, и без цикла повторов приложение начнёт падать под нагрузкой; - сериализация по ключу: очередь с разбиением по идентификатору сущности либо
SELECT ... FOR UPDATE SKIP LOCKEDдля воркеров. Обновления одной записи выстраиваются в линию.
Добавьте про уровень API: ETag и If-Match с ответом 412, чтобы пользователь узнал о чужой правке, а не затёр её формой со старыми данными.
Где спотыкаются
Читают, считают в коде и пишут обратно, считая, что транзакция защитила. Она защищает от грязного чтения, а не от гонки двух транзакций.
Ставят SERIALIZABLE и не пишут обработку отката. Ошибка сериализации это штатная часть работы такого уровня, её надо ловить и повторять с задержкой.
Держат блокировку строки на время вызова внешнего сервиса: транзакция живёт секунды вместо миллисекунд, а соединения в пуле кончаются. Сюда же попытка обойтись synchronized в приложении: при двух инстансах она не значит ничего.