Как гарантировать, что два пользователя не забронируют одно место одновременно?
Что проверяют
Понимаете ли вы, что гарантию даёт база, а не код одного экземпляра сервиса. Интервьюер ждёт, что вы отбросите synchronized и сразу перейдёте к транзакциям и ограничениям. Следом обычно спрашивают, что увидит второй пользователь и как вы выбираете между оптимистичной и пессимистичной блокировкой.
Как отвечать
Начните с того, что сервис работает в нескольких экземплярах, поэтому synchronized и ReentrantLock защищают только память одной JVM. Надёжные варианты:
- ограничение уникальности: таблица броней с
UNIQUE (show_id, seat_id). Вторая вставка упадёт с нарушением ограничения при любой гонке, это последний рубеж, который стоит поставить в любом случае; - условный апдейт:
UPDATE seat SET status = 'BOOKED', user_id = ? WHERE id = ? AND status = 'FREE'и проверка, что обновлена ровно одна строка. Атомарно и без явных блокировок; - пессимистичная блокировка:
SELECT ... FOR UPDATEили@Lock(LockModeType.PESSIMISTIC_WRITE)в Spring Data JPA. Второй запрос ждёт, пока первая транзакция завершится; - оптимистичная блокировка: поле
@Version, при конфликте Hibernate бросаетOptimisticLockException, а Spring оборачивает его вObjectOptimisticLockingFailureException. Хороша, когда конфликты редки.
Для интервалов времени, например брони переговорки, в PostgreSQL есть ограничение EXCLUDE USING gist, которое запрещает пересечение диапазонов. В реальных системах место ещё и временно удерживают: статус «в процессе оплаты» с истечением через несколько минут, а повторный запрос клиента защищают ключом идемпотентности.
Где спотыкаются
Делают проверку и запись двумя шагами: SELECT видит свободное место, затем идёт INSERT. На уровне изоляции READ COMMITTED, который в PostgreSQL стоит по умолчанию, два запроса проходят проверку одновременно.
Предлагают распределённую блокировку в Redis как основное решение. Она добавляет точку отказа и истекающие таймауты, а уникальный индекс решает задачу проще.
Не говорят, что делать с проигравшим. Исключение нужно поймать и вернуть пользователю понятный ответ, например HTTP 409, а не 500.