DEV Community

faangmaster
faangmaster

Posted on

System Design: Ticketmaster

Задача

Нужно сделать дизайн Ticketmaster. Ticketmaster — это онлайн-платформа, которая позволяет пользователям приобретать билеты на концерты, спортивные мероприятия, театральные постановки и другие live ивенты. Число ежедневных активных пользователей ~100M

Решение

Уточняем требования

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

  • Пользователь должен быть способен искать различные события, концерты и т.д.
  • Пользователь должен быть способен смотреть информацию о событии
  • Пользователь должен быть спообен забронировать и оплатить билеты

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

  • Строгая консистентность для бронирования билетов. Два разных пользователя не могут забронировать один и тот же билет.
  • High Availability для поиска и просмотра событий.
  • Число запросов на чтение сильно больше, чем на запись
  • Масштабируемость, чтобы поддержать бронирования на очень популярные события.

Database Design

Ключевые сущности:
Event- информация о событии
Venue - площадка, где будет проводится мероприятие
Ticket - билеты
Performer - информация об исполнителе

API

Поиск события:

GET /search?keyword={keyword}&location={location}&type={type}&start_date={start_date}&end_date={end_date}.
Enter fullscreen mode Exit fullscreen mode

Возвращает список событий Event[].

keyword - текст поиска,
location - населенный пункт, координаты центра и радиус поиска и т.д.
start_date, end_date - даты события
type - тип события

Просмотр события:

GET /event/eventId
Enter fullscreen mode Exit fullscreen mode

возвращает Event, Venue, Performer, Tickets[]

eventId - id события

Бронирование:

Бронирование проходит в два этапа. Сначала мы резервируем билеты, после чего у нас есть ограниченное время (например, пять минут) на их оплату. Если мы не оплатим билеты в течение этого срока, они снова станут доступны другим пользователям.

API для резервирования:

POST /booking/reserve
header JWT | sessionToken
body:{
ticketIds
}
Enter fullscreen mode Exit fullscreen mode

Для оплаты:

POST /booking/confirm
header JWT | sessionToken
body:{
ticketIds
paymentDetails
}
Enter fullscreen mode Exit fullscreen mode

High Level Design

Опишем более детально основные таблицы:

Image description

Из интересного - Ticket должен иметь статус. Статус может быть available, booked, reserved. reserved - означает, что билет зарезервирован, но еще не оплачен. Он может перейти в статус booked, если его оплатили или в available если не оплатили за 5 минут.

High Level Architecture:

Image description

API Gateway отвечает за роутинг запросов от клиента к различным бэкенд сервисам, а также за аутентификацию и rate limiting.

Search сервис отвечает за поиск событий по различным критериям. Для начала он может просто делать SQL-запросы в базу.

Event сервис отвечает за чтение и запись событий в базу данных.

Booking сервис отвечает за резервирование и покупку билетов.

В качестве базы данных следует использовать базу с поддержкой ACID-транзакций, чтобы обеспечить строгую консистентность и исключить возможность бронирования одних и тех же билетов несколькими пользователями. Например, это может быть postgresql или MySQL.

Ticket Reserve - in-memory cache, например, Redis/Memcached. Там мы будем хранить билеты, которые зарезервировали, но еще не оплатили. Если их не оплатят в течении TTL, то они будут удалены из кэша.

Статус reserved не сохраняем в основной базе — остаются только available и booked. При выводе доступных билетов для события выбираем записи со статусом available и отсекаем те, что временно находятся в Ticket Reserve.

При бронировании будет вызван API-метод reserve, который добавит билеты в Ticket Reserve (кэш) с TTL 5 минут. Если пользователь не купит билеты за 5 минут - они будут удалены из Ticket Reserve и станут доступными для других пользователей. Если пользователь купит билеты (confirm API), то они также будут удалены из Ticket Reserve и будет выполнена запись в базу с изменение статуса билетов на booked.

Detailed Design/Deep Dives

Для того, чтобы удовлетворить нефункциональные требования по масштабируемости и high availability нам надо ускорить поиск и Event сервисы.

Для этого вместо SQL-запросов в сервисе Search используем ElasticSearch или Amazon OpenSearch. Данные для индексации будем передавать из основной базы по стриму, например через AWS Kinesis или Kafka. При каждом изменении в базе сразу отправляем update в ElasticSearch, чтобы оперативно переиндексировать записи. Это схема назымается CDC(Change Data Capture).

Также добавим кэш, который будет содержать популярные события.

Добавим еще один полезный функционал. Например, пользователь зашел на сайт и нашел интересующий его концерт. Однако доступных билетов нет, поскольку все они находятся в процессе бронирования другими пользователями. Вместо того чтобы просто сообщить, что билеты недоступны, мы можем поставить пользователя в очередь ожидания.

Такие пользователи будут автоматически нотифицироваться в случае, если кто-то из тех, кто бронировал, не завершил покупку. Это особенно актуально для очень популярных ивентов, билеты на которые раскупаются практически мгновенно.

Для соблюдения честности и порядка, очередь должна храниться в структуре данных типа LinkedHashMap. В качестве ключа — userId, в качестве значения — eventId и timestamp, отражающий момент, когда пользователь встал в очередь. Таким образом, первым в очереди будет пользователь, который подал запрос раньше остальных, и именно он получит приоритет на освободившиеся билеты.
Т.е. можно использовать следующую структуру данных для Waiting Users:
HashMap(key->EventId, value->LinkedHashMap(key->userId, value->timestamp)).

Аналогично для Ticket Reserve:
HashMap(key->EventId, value->LinkedHashMap(key->TicketId, value->timestamp))

Поскольку ожидание может занять продолжительное время, необходимо обеспечить устойчивый канал связи с такими ожидающими клиентами. Это можно реализовать с помощью WebSocket-ов или LongPolling.

Если число ивентов в базе станет большим, то можно партиционировать данные по EventId.

Image description

Waiting Users - in-memory cache. Там мы будем хранить пользователей, которые ожидают появление билетов, если нет доступных билетов.

Top comments (2)

Collapse
 
negenerat profile image
negenerat • Edited

Спасибо за статью!
Есть пару вопросов:

  1. Действительно ли стоит делать партиции по eventId, кажется, что лучше по vendorId, иначе для получения событий какой-то площадки нужно будет ходить по разным партициям. Хотелось бы узнать мнение, какие плюсы и минусы
  2. Если TTL 5 минут, а на 2 минуте in memory хранилище отвалится, то брони ведь слетят, а в базе статуса брони нет, получается, что гарантии нет
  3. При получении доступных мест, нужно исключить забронированные тикеты, как это будет происходить? в кеше будут данные храниться в виде - reserved_tickets: set(), когда пользователь делает запрос, получаем из кеша все id тикетов, далее в базе ищем через id NOT IN(...), да?
Collapse
 
faangmaster profile image
faangmaster • Edited
  1. Вы наверное имеете ввиду venueId (id площадки). Тут нужно смотреть, какие запросы чаще. Если, например, вам нужно получить все площадки, в которых идет фильм X, то при партиционировании по venueId вам нужно будет также смотреть все партиции. Но при большом числе данных (когда это партиционирование и придется делать) у нас будет ElasticSearch для поиска, а в базу будем ходить когда надо делать CRUD операции по билетам и ивентам. Поиск по площадке или по названию мероприятия будет через ElasticSearch, а не через базу.
  2. В таком случае слетят не брони, а резервации. Т.е. когда человек выбрал билеты, но еще не оплатил. Для fault tolerance Ticket Reserve может иметь несполько primary и replica партиций, которые находятся на разных серверах.
  3. Из базы/ElasticSearch получаем все билеты, которые available, потом отфильтровуем те, которые в резерве. Но согласен, можно помечать их в базе как reserved. Трейдоф в том, что это увеличит число апдейтов базы, но при этом позволит восстановить статус при падении Ticket Reserve. Вопрос в том, насколько это важно.