Задача о двух пассажирах и одном билете - Академия Selectel

Задача о двух пассажирах и одном билете

Артём Шумейко
Артём Шумейко Senior Python Backend-разработчик
5 декабря 2024

Будет интересна разработчикам, знакомым с 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 минут.