Как обеспечить консистентность данных при параллельных бронированиях?
Что проверяют
Понимаете ли вы, где возникает гонка при бронировании и какой механизм базы её закрывает. Классика: два запроса одновременно увидели, что место свободно, и оба его заняли. Следом спрашивают, что выбрать при высокой конкуренции за один ресурс и как решение ведёт себя на нескольких инстансах.
Как отвечать
Сначала объясните, почему @Transactional сам не спасает: на READ COMMITTED, уровне по умолчанию в PostgreSQL, обе транзакции прочитают «свободно» и обе запишут бронь. synchronized в Java тоже бесполезен, как только инстансов больше одного.
Варианты:
- ограничение в базе: уникальный индекс по
(room_id, date), а для интервалов в PostgreSQL exclusion constraintEXCLUDE USING gistс пересечением диапазонов&&(для равенства поroom_idнужно расширениеbtree_gist). Второй запрос получит ошибку, её вы превращаете в ответ «уже занято». Самый надёжный вариант; - атомарный условный апдейт:
UPDATE seats SET status = 'BOOKED' WHERE id = ? AND status = 'FREE'и проверка числа изменённых строк, без чтения перед записью; - пессимистическая блокировка:
SELECT ... FOR UPDATE, в JPA@Lock(LockModeType.PESSIMISTIC_WRITE). Подходит при частых конфликтах, если транзакция короткая и задан таймаут ожидания; - оптимистическая блокировка: поле
@Version, при конфликтеOptimisticLockExceptionи повтор. Хороша, когда конфликты редки; - SERIALIZABLE: база сама найдёт конфликт, но приложение обязано повторять транзакцию при ошибке сериализации.
Для горячих ресурсов вроде билетов на концерт добавляют временное удержание места с истечением срока. Между сервисами 2PC сейчас почти не используют, вместо него сага и outbox.
Где спотыкаются
Делают «проверил, потом вставил» двумя запросами и считают, что транзакция всё решила.
Берут распределённую блокировку в Redis как основное решение. Без fencing token она не гарантирует взаимоисключение при паузах GC и сетевых задержках, а ограничение в базе даёт гарантию даром.
Выбирают оптимистическую блокировку для ресурса, за который бьются сотни пользователей: большинство транзакций уходит в повторы.