Задача 6 из 10
Бронирование мест
Спроектируйте бронирование мест на сеанс: кинотеатр или концерт, где сотни человек одновременно пытаются занять одно и то же место.
Функциональные требования
- Показать свободные места на сеанс
- Занять место на время оплаты
- Подтвердить бронь после успешной оплаты
- Освободить место, если оплата не прошла
Нефункциональные требования
- Пик: 10 000 попыток брони в секунду на популярный сеанс
- Двойная продажа одного места недопустима ни при каких условиях
- Место держится за пользователем 10 минут
- Карта мест может отставать на секунды, сама бронь — нет
Нарисуйте архитектуру
Не гонитесь за красотой: на собеседовании от схемы нужно, чтобы по ней было видно путь запроса и где лежат данные. Сначала нарисуйте сами — эталон и разбор ниже.
Блок из палитры — добавить. Тащите мышкой или пальцем. Двойной клик по названию — переименовать. «Связь» — щёлкнуть по одному блоку, потом по другому: получится стрелка.
Схема сохраняется в этом браузере — вкладку можно закрыть и вернуться позже.
1 С чего начать, если ступор
- Это задача про конкурентность, а не про объём: начните с того, где именно возникает гонка.
- Проверка «свободно ли» и последующая запись — не атомарны. Отсюда все проблемы.
- Оптимистическая блокировка (версия строки) против пессимистической (SELECT FOR UPDATE) — назовите цену обеих.
- Временная бронь должна истекать сама, а не по крону, который может не отработать.
- Оплата — внешний сервис: он может ответить через минуту, не ответить вовсе или ответить дважды.
2 Эталонная схема
Единственный источник правды о занятости — БД: место занимается внутри транзакции, а не проверкой «свободно ли» с последующей записью. Redis отдаёт быструю карту мест для отрисовки, но решение о том, кому досталось место, принимает не он.
Это один из рабочих вариантов, а не единственно верный. Если у вас иначе, но вы можете объяснить почему — на собеседовании это ровно то, что нужно.
3 Разбор: что должно прозвучать
Где именно гонка
- SELECT «свободно?» → INSERT брони: между двумя запросами успевает вклиниться другой клиент
- Уникальный индекс (сеанс, место) — последний рубеж, который не даст задвоить даже при ошибке в коде
- Ответ «место только что заняли» — нормальный сценарий, а не исключительная ситуация
Блокировки
- Пессимистическая (SELECT ... FOR UPDATE): просто и надёжно, но держит блокировку и не любит длинные транзакции
- Оптимистическая (версия строки, UPDATE ... WHERE version = ?): дешевле, но требует ретраев на конфликте
- На 10k попыток в секунду по одному сеансу важно, чтобы транзакция была максимально короткой — никаких вызовов платёжки внутри неё
Временная бронь
- Срок брони хранится в самой записи: истечение проверяется при чтении, а не только фоновым процессом
- Фоновое освобождение — оптимизация, а не гарантия: система должна быть корректной и без него
- Отложенная задача на 10 минут удобнее крона, который проходит раз в минуту
Оплата
- Платёж вызывается вне транзакции БД — иначе блокировка живёт секунды или минуты
- Вебхук идемпотентен: провайдер пришлёт его дважды
- Отдельный сценарий: деньги списаны, а бронь уже истекла — нужен возврат или продление
Карта мест и кэш
- Кэш карты мест всегда немного отстаёт — по требованиям это допустимо
- Принимать решение о брони по кэшу нельзя, только показывать
- Клиенту стоит явно показывать, что место может уйти, пока он думает
Куда копать дальше, если спросят
- Очередь ожидания на сверхпопулярный сеанс вместо честной гонки
- Бронь группы мест рядом: атомарность для нескольких мест сразу
- Защита от ботов, скупающих места
- Что делать при отмене сеанса: массовые возвраты
Следующая задача: Поиск по каталогу
Отдельное хранилище для поиска, его наполнение из основной БД и жизнь с тем, что индекс всегда чуть отстаёт.