Задача о двух пассажирах и одном билете
Будет интересна разработчикам, знакомым с Python и PostgreSQL.
Задачу подготовил Артём Шумейко — внештатный райтер, амбассадор Selectel и автор YouTube-канала о разработке.
Условие
На сайте авиакомпании произошел сбой. Два человека одновременно забронировали последнее оставшееся место в самолете — и оба оплатили билет. Но затем один из пассажиров получил ошибку о том, что его место уже занято. В итоге авиакомпания понесла репутационные и финансовые потери.
Отдел серверной разработки передал вам подробности об устройстве базы данных. Что известно?
В рамках системы бронирования авиабилетов есть база данных с двумя сущностями:
Users:
- id — уникальный идентификатор пользователя.
Tickets:
- id — уникальный идентификатор билета;
- from — место вылета;
- to — место назначения;
- place — номер сидения в самолете;
- flight_at — дата и время рейса;
- reserved_at — время последнего бронирования;
- reserved_by — идентификатор пользователя, который последним забронировал билет;
- owner — идентификатор пользователя, купившего билет (может быть NULL, если билет только забронирован).
В таблице Tickets изначально вбиты все билеты на конкретный рейс, то есть новые записи в базе данных не появляются.
Также у вас есть участок кода с бэкенд-сервиса, отвечающего за бронирование. Он написан на Python:
place = "4A"
flight_id = "AS579"
user_id = 123
ticket_query = "SELECT * FROM tickets WHERE place=%place AND flight_id=%flight_id"
ticket = db.execute(ticket_query, (place, flight_id))
if ticket.reserved_at < now() - 10 * MINUTE:
reserve_ticket_query = "UPDATE tickets SET reserved_by=%user_id WHERE id=%ticket_id"
db.execute(reserve_ticket_query, (user_id, ticket))
return "Можете оплатить билет по ссылке: https://..."
else:
return "Билет забронирован другим пассажиром"
Задача
Предложите решение, которое запретит нескольким пользователям одновременно бронировать одно место.
Подсказка: решение может быть основано на работе с базой данных, а именно на изменении SQL-запроса, который отправляется в PostgreSQL.
Решение
Чтобы исключить ситуацию, при которой два пользователя одновременно забронируют одно место, необходимо использовать механизм блокировки строк в базе данных. Это достигается с помощью SQL-запроса с конструкцией SELECT FOR UPDATE. Такой подход позволяет заблокировать строку с билетом до завершения текущей транзакции. Ниже представлен обновленный код и пояснение.
Достаточно изменить SELECT-запрос на следующий:
ticket_query = "SELECT * FROM tickets WHERE place=%place AND flight_id=%flight_id FOR UPDATE"
Обратите внимание на FOR UPDATE в конце запроса. Это значит, что транзакция, которая раньше получит текущий билет, не даст другим транзакциям его заблокировать. То есть если одновременно несколько транзакций попробуют сделать SELECT … FOR UPDATE, то самая быстрая, которая первой выполнит запрос, заблокирует строку — остальным придется ждать ее завершения. Получается, что первая зарезервирует билет, а остальные, дойдя до условия if ticket.reserved_at < now() — 10 * MINUTE:, не зайдут в него, так как поле reserved_at только что обновилось и с этого момента еще не прошло 10 минут.