← все задачи

Задача 6 из 10

Бронирование мест

Спроектируйте бронирование мест на сеанс: кинотеатр или концерт, где сотни человек одновременно пытаются занять одно и то же место.

Уровень: Средний На собеседовании: 45–60 минут блокировкитранзакциивременная бронь

Функциональные требования

  • Показать свободные места на сеанс
  • Занять место на время оплаты
  • Подтвердить бронь после успешной оплаты
  • Освободить место, если оплата не прошла

Нефункциональные требования

  • Пик: 10 000 попыток брони в секунду на популярный сеанс
  • Двойная продажа одного места недопустима ни при каких условиях
  • Место держится за пользователем 10 минут
  • Карта мест может отставать на секунды, сама бронь — нет

Нарисуйте архитектуру

Не гонитесь за красотой: на собеседовании от схемы нужно, чтобы по ней было видно путь запроса и где лежат данные. Сначала нарисуйте сами — эталон и разбор ниже.

Блок из палитры — добавить. Тащите мышкой или пальцем. Двойной клик по названию — переименовать. «Связь» — щёлкнуть по одному блоку, потом по другому: получится стрелка.

Схема сохраняется в этом браузере — вкладку можно закрыть и вернуться позже.

1 С чего начать, если ступор
  • Это задача про конкурентность, а не про объём: начните с того, где именно возникает гонка.
  • Проверка «свободно ли» и последующая запись — не атомарны. Отсюда все проблемы.
  • Оптимистическая блокировка (версия строки) против пессимистической (SELECT FOR UPDATE) — назовите цену обеих.
  • Временная бронь должна истекать сама, а не по крону, который может не отработать.
  • Оплата — внешний сервис: он может ответить через минуту, не ответить вовсе или ответить дважды.
2 Эталонная схема

Единственный источник правды о занятости — БД: место занимается внутри транзакции, а не проверкой «свободно ли» с последующей записью. Redis отдаёт быструю карту мест для отрисовки, но решение о том, кому досталось место, принимает не он.

Это один из рабочих вариантов, а не единственно верный. Если у вас иначе, но вы можете объяснить почему — на собеседовании это ровно то, что нужно.

3 Разбор: что должно прозвучать

Где именно гонка

  • SELECT «свободно?» → INSERT брони: между двумя запросами успевает вклиниться другой клиент
  • Уникальный индекс (сеанс, место) — последний рубеж, который не даст задвоить даже при ошибке в коде
  • Ответ «место только что заняли» — нормальный сценарий, а не исключительная ситуация

Блокировки

  • Пессимистическая (SELECT ... FOR UPDATE): просто и надёжно, но держит блокировку и не любит длинные транзакции
  • Оптимистическая (версия строки, UPDATE ... WHERE version = ?): дешевле, но требует ретраев на конфликте
  • На 10k попыток в секунду по одному сеансу важно, чтобы транзакция была максимально короткой — никаких вызовов платёжки внутри неё

Временная бронь

  • Срок брони хранится в самой записи: истечение проверяется при чтении, а не только фоновым процессом
  • Фоновое освобождение — оптимизация, а не гарантия: система должна быть корректной и без него
  • Отложенная задача на 10 минут удобнее крона, который проходит раз в минуту

Оплата

  • Платёж вызывается вне транзакции БД — иначе блокировка живёт секунды или минуты
  • Вебхук идемпотентен: провайдер пришлёт его дважды
  • Отдельный сценарий: деньги списаны, а бронь уже истекла — нужен возврат или продление

Карта мест и кэш

  • Кэш карты мест всегда немного отстаёт — по требованиям это допустимо
  • Принимать решение о брони по кэшу нельзя, только показывать
  • Клиенту стоит явно показывать, что место может уйти, пока он думает

Куда копать дальше, если спросят

  • Очередь ожидания на сверхпопулярный сеанс вместо честной гонки
  • Бронь группы мест рядом: атомарность для нескольких мест сразу
  • Защита от ботов, скупающих места
  • Что делать при отмене сеанса: массовые возвраты

Следующая задача: Поиск по каталогу

Отдельное хранилище для поиска, его наполнение из основной БД и жизнь с тем, что индекс всегда чуть отстаёт.