Перевод на русский язык статьи Design Ticketmaster
Видеоразбор этой задачи на русском языке можно посмотреть здесь - https://www.youtube.com/watch?v=zxeR5bfsNOg
Постановка задачи
🎟️ Что такое Ticketmaster?
Ticketmaster - это онлайн-платформа, позволяющая пользователям
приобретать билеты на концерты, театральные постановки,
спортивные и другие мероприятия.
Функциональные требования
- В начале интервью определите функциональные и нефункциональные > требования. Для пользовательских приложений функциональные требования - это формулировки вида "Пользователь может...", а нефункциональные - это характеристики системы вида "Система должна...".
- Приоритизируйте 3-4 ключевых функциональных требования. Все остальные требования показывают что вы обладаете продуктовым мышлением, но явно обозначьте это "за рамками задачи", чтобы интервьюер понимал, что эти пункты не входят в дизайн. Уточните, не хочет ли интервьюер увеличить/уменьшить приоритет какого-то требования. Выбор только 3-4 требований помогает оставаться сфокусированным и уложиться во временные рамки интервью.
Основные требования
- Пользователи могут просматривать мероприятия.
- Пользователи могут искать мероприятия.
- Пользователи могут бронировать билеты на мероприятия.
За рамками задачи
- Пользователи могут просматривать свои бронирования.
- Администраторы или организаторы могут добавлять мероприятия.
- Для популярных мероприятий есть динамическое ценообразование.
Нефункциональные требования
Основные требования
- Система должна отдавать приоритет доступности при поиске и просмотре мероприятий и согласованности при бронировании, чтобы избежать двойных бронирований.
- Система должна быть масштабируемой и способной обрабатывать высокую нагрузку для популярных мероприятий, например 10 млн пользователей для одного события.
- Система должна обеспечивать низкую задержку поиска (< 500 мс).
- Система ориентирована на чтение и должна поддерживать высокую пропускную способность чтения, соотношение чтения:записи примерно 100:1.
За рамками задачи
- Система должна защищать пользовательские данные и соответствовать GDPR.
- Система должна быть отказоустойчивой.
- Система должна обеспечивать безопасные транзакции для покупок.
- Система должна быть хорошо протестирована и легко разворачиваться (CI/CD).
- Система должна иметь регулярные резервные копии.
На доске это может выглядеть примерно так:
Описание требований за рамками задачи показывает продуктовое
мышление и дает интервьюеру возможность переопределить
приоритеты. Но это все же необязательная вещь, если
дополнительные идеи не приходят в голову сразу, не тратьте время > и двигайтесь дальше.
Подготовка
Планирование подхода
Прежде чем переходить к проектированию системы, важно на секунду остановиться и продумать стратегию. К счастью, для "продуктовых" задач план обычно простой: последовательно собирать дизайн, проходя по функциональным требованиям одно за другим. Так вы сохраните фокус и не утонете в деталях.
Когда функциональные требования удовлетворены, используйте нефункциональные требования, чтобы определить направления для погружения в детали, где это необходимо.
Проектирование API
Начнем с определения основных сущностей, это поможет спроектировать API. Пока не обязательно знать каждое поле или колонку, но если у вас уже есть представление о том, что там будет - можно это записать.
Для основных функциональных требований понадобятся следующие сущности:
- Event (Мероприятие): хранит основную информацию о мероприятии, включая дату, описание, тип и исполнителя или команду.
- User (Пользователь): представляет человека, взаимодействующего с системой. Дополнительных пояснений не требуется.
- Performer (Исполнитель): представляет индивидуального исполнителя или группу, выступающую или участвующую в мероприятии. Ключевые атрибуты включают имя исполнителя, краткое описание и, возможно, ссылки на работы или профили.
- Venue (Площадка): представляет физическое место проведения мероприятия. Каждая сущность площадки включает адрес, вместимость и конкретную карту мест, предоставляющую расположение мест, уникальное для площадки.
- Ticket (Билет): хранит информацию, связанную с отдельными билетами на мероприятия. Включает атрибуты, такие как идентификатор мероприятия, детали места (секция, ряд, номер места), цена и статус (доступен или продан). При создании нового мероприятия создается билет для каждого места на площадке на основе карты мест площадки. Сама карта мест хранится как часть сущности Venue (например, JSON-структура или связанная таблица, определяющая секции, ряды и номера мест вместе с координатами для отрисовки). Клиент использует эти данные карты мест в сочетании со статусом каждого билета для отрисовки интерактивного интерфейса выбора мест.
- Booking (Бронирование): записывает детали покупки билетов пользователем. Обычно включает идентификатор пользователя, список идентификаторов билетов, общую цену и статус бронирования (например, в процессе или подтверждено). Эта сущность ключевая для управления транзакционным аспектом процесса покупки билетов.
Можно было бы объединить данные бронирования с сущностью Ticket,
но отдельная сущность Booking полезна, когда пользователь
покупает несколько билетов в одной транзакции, поскольку она
объединяет их в рамках одного заказа с общим статусом оплаты и
общей ценой.
В реальном интервью достаточно короткого списка как выше - главное проговорить сущности и убедиться, что вы и интервьюер одинаково их понимаете.
Дальше наша цель проста: собрать дизайн, который удовлетворяет функциональным и нефункциональным требованиям. Мы идем последовательно: сначала закрываем функциональные требования, затем усиливаем дизайн нефункциональными.
API для просмотра мероприятий прост. Создаем простой GET эндпоинт, принимающий id и возвращающий детали этого мероприятия.
GET /events/:id -> Event & Venue & Performer & Ticket[]
// билеты используются для отрисовки карты мест на клиенте
В большинстве случаев API и сущности самоописательны и интервьюер
сам понимает, какие данные используются в API. Вы можете
уточнить, хочет ли интервьюер более подробной информации, но
будьте осторожны с избыточной многословностью - нам нужно покрыть
много тем, и перечисление полей объекта Event может быть не
лучшим использованием времени.
Далее, для поиска нам нужен один GET эндпоинт, принимающий набор параметров поиска и возвращающий список мероприятий, соответствующих этим параметрам.
GET /events/search?keyword={keyword}&start={start_date}&end={end_date}&pageSize={page_size}&page={page_number} -> Event[]
Когда речь заходит о покупке/бронировании билета, у нас есть POST эндпоинт, который принимает список билетов и детали оплаты и возвращает bookingId.
Позже в дизайне мы превратим это в два отдельных эндпоинта - один для резервирования билета и один для подтверждения покупки, но это хорошая отправная точка.
POST /bookings/:eventId -> bookingId
{
"ticketIds": string[],
"paymentDetails": ...
}
Это нормально начинать с простых API и развивать их по мере
продвижения и уточнения дизайна. Достаточно сказать: "Вот простой
API для старта, позже мы его скорректируем, чтобы покрыть более
сложные сценарии".
Высокоуровневое проектирование
1. Пользователи могут просматривать мероприятия
Когда пользователь переходит на /events/:id, он должен видеть детали мероприятия включая карту мест с отображением доступности. На странице также отображаются название и описание мероприятия. Может быть представлена ключевая информация, такая как местоположение, даты мероприятия и факты об исполнителях или командах.
Мы начинаем с разметки основных компонентов для взаимодействия между клиентом и нашими сервисами. Добавим сервис мероприятий, который подключается к базе данных, хранящей данные о мероприятиях, площадках и исполнителях, описанных в основных сущностях выше. Этот сервис будет обрабатывать чтение/просмотр мероприятий.
Давайте пройдем по шагам, что происходит, когда пользователь переходит к просмотру мероприятия:
- Клиент делает REST GET запрос с
idмероприятия. - API-шлюз затем перенаправляет запрос в сервис мероприятий.
- Сервис мероприятий запрашивает в базе данных информацию о мероприятии, площадке и исполнителях и возвращает результаты клиенту.
Компоненты:
- Клиенты: пользователи будут взаимодействовать с системой через веб-сайт или приложение клиента. Все клиентские запросы маршрутизируются в бэкенд системы через API-шлюз.
- API-шлюз: служит точкой входа для клиентов для доступа к различным сервисам системы. Отвечает в основном за маршрутизацию запросов к соответствующим сервисам, но также может быть настроен для обработки сквозной функциональности, такой как аутентификация, ограничение частоты запросов и логирование.
- Сервис мероприятий: ответственен за обработку запросов путем получения необходимой информации о мероприятии, площадке и исполнителях из базы данных и возврата результатов клиенту.
- База данных: хранит таблицы мероприятий, исполнителей и площадок.
2. Пользователи могут искать мероприятия
Теперь у нас есть базовая функциональность для просмотра мероприятия. Но как пользователи вообще найдут мероприятия? Когда пользователи впервые открывают ваш сайт, они ожидают возможность поиска предстоящих мероприятий. Этот поиск будет параметризован на основе любой комбинации ключевых слов, артистов/команд, местоположения, даты или типа мероприятия.
Начнем с самого базового варианта - создадим простой сервис, принимающий поисковые запросы. Этот сервис подключится к вашей базе данных и будет выполнять запросы, фильтруя данные в соответствии с параметрами. У этого подхода есть проблемы, но это хорошая стартовая точка. Мы обсудим варианты оптимизации, когда будем погружаться в детали.
Когда пользователь ищет мероприятие:
- Клиент делает REST GET запрос с параметрами поиска.
- API-шлюз после проверки аутентификации и ограничения частоты пересылает запрос в сервис поиска.
- Сервис поиска запрашивает в базе данных мероприятия, соответствующие параметрам поиска, и возвращает их клиенту.
3. Пользователи могут бронировать билеты на мероприятия
Главное, чего мы стараемся избежать - это два пользователя, заплативших за один и тот же билет. Это создало бы неловкую ситуацию на мероприятии. Чтобы обработать эту проблему согласованности, нам нужно выбрать базу данных, поддерживающую транзакции, такую, как PostgreSQL. Это позволит нам
гарантировать, что только один пользователь может забронировать билет за раз.
Дополнительно нам нужно реализовать надлежащие уровни изоляции и либо блокировку на уровне строк, либо оптимистичный контроль
конкурентности
(OCC) для полного предотвращения двойных бронирований. Мы обсудим это подробнее в разделе Погружение в детали.
Это наглядный пример случая, когда высокая конкурентность может
привести к плохим результатам, таким как двойные бронирования.
Управление конкуренцией - это паттерн, который появляется
во многих задачах на проектирование систем, поэтому стоит изучить
его глубже.
Простая реализация бронирования
-
Новые таблицы: сначала добавляем две новые таблицы в базу данных:
BookingsиTickets. ТаблицаBookingsбудет хранить детали каждого бронирования, включая идентификатор пользователя, идентификаторы билетов, общую цену и статус бронирования. ТаблицаTicketsбудет хранить детали каждого билета, включая идентификатор мероприятия, детали места, цену и статус. ТаблицаTicketsтакже будет иметь колонкуbooking_id, связывающую ее с таблицейBookings. -
Сервис бронирований: отвечает за основную функциональность процесса бронирования билетов. Он использует таблицы
BookingsиTicketsдля получения, обновления или сохранения соответствующих данных. Он также взаимодействует с платежной системой для обработки платежей. После подтверждения оплаты сервис бронирования обновляет статус билета на "sold". - Платежная система: внешний сервис, ответственный за обработку платежных транзакций. После обработки платежа он уведомляет сервис бронирования о статусе транзакции.
Когда пользователь бронирует билет, происходит следующее:
- Пользователь перенаправляется на страницу бронирования, где может ввести данные для оплаты и подтвердить бронирование.
- При подтверждении отправляется POST запрос на эндпоинт
/bookingsс выбранными идентификаторами билетов. - Сервер бронирования инициирует транзакцию для:
- проверки доступности выбранных билетов
- обновления статуса выбранных билетов на "booked"
- создания новой записи бронирования в таблице
Bookings
Если транзакция успешна, сервер бронирования возвращает успешный ответ клиенту. В противном случае, если транзакция не удалась, например, потому что другой пользователь уже забронировал билет в то же самое время, мы возвращаем информацию об ошибке клиенту.
Обратите внимание: это означает, что при создании нового
мероприятия нам нужно
создать новый билет для каждого места на площадке. Каждый из них
будет доступен для покупки до тех пор, пока не будет
забронирован.Вы можете заметить, что несколько сервисов используют одну базу
данных. Правило "одна база данных на сервис" часто повторяется,
но это не жесткое правило. Многие крупнейшие компании мира
используют общие базы данных между сервисами, когда это имеет
смысл. Здесь общая база данных - правильный выбор, потому что
данные тесно связаны (бронирования нуждаются в билетах, билеты
нуждаются в мероприятиях), нам нужны ACID транзакции для
бронирования, и разделение баз данных добавило бы сложности без
реальной пользы. На собеседовании вам следует взвешивать
компромиссы и принимать осмысленные решения, а не повторять
архитектурные догмы.
Вы могли заметить фундаментальную проблему с этим дизайном. Пользователи могут попасть на страницу бронирования, ввести данные для оплаты и затем узнать, что билет, который они хотели, больше не доступен. Это плохой пользовательский опыт, и мы обсудим, как этого избежать чуть позже. Пока у нас простая реализация, удовлетворяющая функциональному требованию.
Потенциальные погружения в детали
После того как мы удовлетворили основные функциональные требования, настало время детальнее углубиться в нефункциональные требования.
Степень, с которой кандидат должен проактивно вести детальное
обсуждение, зависит от его уровня. Например, на собеседовании
уровня Middle вполне разумно, что интервьюер задает вопросы по
деталям реализации. Однако на собеседованиях уровня Senior и
Staff+ ожидаемый уровень инициативы и ответственности кандидата
возрастает. Они должны уметь самостоятельно видеть проблемы в
дизайне и предлагать решения.
1. Как улучшить опыт бронирования путем резервирования билетов?
Текущее решение технически работает, но приводит к плохому пользовательскому опыту. Никто не хочет тратить 5 минут на заполнение формы оплаты, только чтобы узнать, что билеты, которые они хотели, больше не доступны.
Если вы пользовались похожими сервисами для покупки билетов на мероприятия, авиабилетов или бронирования отелей, вы видели таймер обратного отсчета на завершение покупки. Это распространенная техника резервирования билетов для пользователя во время оформления заказа. Давайте обсудим, как можем добавить что-то подобное в наш дизайн.
Нам нужно обеспечить, чтобы билет был зарезервирован для определенного пользователя во время оформления заказа. Также нужно обеспечить, чтобы если пользователь бросит процесс оформления, билет освобождался для покупки другими пользователями. Наконец, нужно обеспечить, чтобы при завершении оформления статус билета менялся на "sold" и бронирование подтверждалось. Вот несколько
способов, как мы можем это сделать:
Подход Плохое решение, которое многие кандидаты предлагают для этой проблемы - использование долгоживущих блокировок базы данных (иногда называемых "интерактивными транзакциями"). При этом подходе база данных напрямую используется для блокировки конкретной строки в таблице билетов, обеспечивая эксклюзивный доступ первому пользователю, пытающемуся забронировать билет. Это обычно делается с помощью оператора Когда речь идет о снятии блокировки, есть два случая для рассмотрения: 1. Если пользователь завершает покупку, транзакция фиксируется, блокировка в базе данных снимается, и статус билета устанавливается в "booked". 2. Если пользователь слишком долго тянет или бросает процесс бронирования, система должна полагаться на их последующие действия или таймауты сессии для снятия блокировки. Это вносит риск бесконечной блокировки билетов при ненадлежащей обработке. Проблемы Почему это плохая идея? Блокировки базы данных предназначены для использования на короткие периоды времени. Держать транзакцию открытой долгое время (например, 5-минут) обычно не рекомендуется. Это может неэффективно использовать ресурсы базы данных и увеличивать риск конкуренции за блокировки и риск возникновенияПлохое решение: Долгоживущие блокировки в базе данных
SELECT FOR UPDATE в PostgreSQL, который
блокирует выбранные строки как часть транзакции базы данных. Блокировка строки сохраняется до тех пор, пока транзакция не будет зафиксирована или откачена. В течение этого времени другие транзакции, пытающиеся выбрать ту же строку с SELECT FOR UPDATE, будут заблокированы до снятия блокировки. Это гарантирует,
что только один пользователь может обработать бронирование билета за раз.
взаимоблокировок. Хотя PostgreSQL поддерживает lock_timeout для отказа в транзакциях, слишком долго ожидающих блокировки, это не элегантное решение для нашего случая, потому что пользователи увидят ошибку вместо того, чтобы быть поставленными в очередь. Реализация таймаута потребует управления на уровне приложения и вносит дополнительные сложности. Наконец, этот подход может плохо
масштабироваться при высокой нагрузке, поскольку длительные блокировки могут привести к увеличению времени ожидания других пользователей и стать потенциальным узким местом производительности. Обработка крайних случаев, таких
как сбои приложения или сетевые проблемы, становится более сложной, так как они могут оставить блокировки в неопределенном состоянии.
Подход Есть решение лучше - заблокировать билет, добавив поле Теперь подумаем, как обрабатывать разблокировку с этим подходом: 1. Если пользователь завершает покупку, статус меняется на "booked", и блокировка снимается. 2. Если пользователь слишком долго тянет или бросает покупку, статус меняется обратно на "available" по достижении времени истечения, и блокировка снимается. Сложная часть здесь - как обрабатывать время истечения. Мы могли бы использовать Cron Job для периодического запроса строк со статусом "reserved", где прошедшее время превышает длительность блокировки, и затем вернуть их в "available". Это намного лучше, но будет некоторая задержка между истечением времени резервирования и моментом времени когда Cron Job вернет статус строки на "available". В идеале, особенно для популярных мероприятий, блокировка должна сниматься моментально после истечения. Проблемы Подход с Cron Job имеет 2 существенных недостатка:Хорошее решение: Статус и время истечения с Cron Job
status и expires_at в таблицу билетов. Билет может находиться в 1 из 3 состояний: "available", "reserved", "booked". Это позволяет отслеживать статус каждого билета и автоматически снимать блокировку по достижении времени истечения. Когда пользователь выбирает билет, статус меняется с "available" на "reserved", и в
expires_at записывается текущая метка времени + таймаут резервирования (например, 10 минут).
Подход Мы можем сделать еще лучше, чем наше решение на основе Cron, заметив, что статус доступности любого билета - это один из двух вариантов: "available" ИЛИ "reserved", но время резервирования истекло. В таком случае мы можем создавать короткие транзакции для обновления полей в записи билета (например, изменение Таким образом, в псевдокоде наша транзакция выглядит так: 1. Начинаем транзакцию. 2. Проверяем, доступен ли текущий билет: "available" ИЛИ ("reserved", но истек). 3. Обновляем 4. Фиксируем транзакцию. Это гарантирует, что только один пользователь сможет зарезервировать билет, причем билет становится доступным сразу же после истечения времени Проблемы Наши операции чтения будут немного медленнее из-за необходимости фильтрации по двум значениям. Мы можем частично решить это, используя материализованные представления или другие возможности современных СУБД вместе с составным индексом. Наша таблица в базе данных также менее читабельна для других потребителей данных, поскольку некоторые резервации на самом деле истекли. МыОтличное решение: Неявный статус со status и expires_at
"available" на "reserved" и установка времени истечения на +10 минут). Внутри этих транзакций мы можем подтвердить, что билет доступен перед резервированием или что предыдущее резервирование истекло.status на "reserved", а expires_at на текущее время + 10 минут.
резервирования.
можем решить эту проблему, используя Cron Job или периодическую очистку, как рассказывалось выше, с очень важной разницей: поведение нашей системы не будет затронуто, если эта очистка задержится.
Подход Другое отличное решение - реализовать распределенную блокировку с TTL (Time To Live, время жизни) с использованием распределенной системы вроде Redis. Вы можете задаться вопросом: если PostgreSQL уже обеспечивает строгую согласованность, зачем вообще нужен Redis? Ключевая причина в том, что нам нужно временное резервирование, которое автоматически истекает. PostgreSQL изначально не поддерживает TTL на уровне строк - потребовалась бы логика истечения на Вот как это будет работать: 1. Когда пользователь выбирает билет, берем блокировку в Redis с уникальным идентификатором (например, ID билета) с предопределенным TTL. Этот TTL действует как автоматическое время истечения блокировки. 2. Если пользователь завершает покупку, статус билета в базе данных обновляется на "booked", и блокировка в Redis вручную освобождается кодом приложения до истечения TTL. 3. Если TTL истекает (указывая, что пользователь не завершил покупку вовремя), Redis автоматически освобождает блокировку и билет становится доступным для бронирования другими пользователями. Теперь наша таблица У нас также нет состояния гонки при получении блокировки: команда Redis Проблемы Сложность при чтении: поскольку резервирования живут в Redis, а не в базе данных, сервису мероприятий нужен способ показывать зарезервированные места как недоступные на карте мест. Один подход - запрашивать Redis для всех заблокированных ID билетов для данного мероприятия (используя Redis Set с ключом Обработка сбоев: если наша распределенная блокировка по какой-либо причине выйдет из строя, будет период, когда пользовательский опыт ухудшится. Обратите внимание, что мы никогда не получим "двойное бронирование", поскольку наша база данных будет использовать OCC или блокировку на уровне строк для этого. Истечение TTL во время оплаты: что если TTL блокировки истекает во время обработки платежа? Если блокировка пользователя A истекает на 10-й минуте, но его оплата завершается на 11-й, пользователь B мог перехватить блокировку между этим. В этом редком сценарии транзакция в базе данных в шаге 7 (см. далее) неОтличное решение: Распределенная блокировка с TTL
уровне приложения (подход с Cron выше). Redis дает встроенное автоматическое истечение ключей, и поскольку он целиком находится в памяти, получение и освобождение блокировки чрезвычайно быстры при высокой конкурентности.Tickets имеет только два состояния: "available" и "booked". Блокировка зарезервированных билетов полностью обрабатывается Redis. Ключом в Redis будет ID билета, а значение - ID пользователя. Таким образом мы можем убедиться, что при подтверждении бронирования пользователь - тот, кто зарезервировал билет.SET key value NX EX seconds атомарна, поэтому только один клиент успешно установит ключ. Для многобилетных бронирований (пользователь выбирает несколько мест) можно получать блокировки последовательно для каждого билета. Если любая блокировка не удалась, освобождаем уже полученные. Использование Lua-скрипта в
Redis может сделать получение нескольких блокировок атомарным, если билеты хешируются на один узел Redis.event:{eventId}:reserved, который обновляется вместе с каждой блокировкой). Это добавляет сетевой запрос в Redis при чтении, но на практике это быстро. Альтернативно можно делать write-through статуса "reserved" в базу данных при получении блокировки, считая TTL Redis источником истины для истечения блокировки и используя периодическую очистку для удаления устаревших резервирований в базе данных. В любом случае это стоит упомянуть на
собеседовании.
Недостаток только в том, что пользователи могут получить ошибку после заполнения данных для оплаты, если кто-то их опередит. Это неприятно, но это лучше, чем когда все билеты выглядят недоступными, как было бы при сбое Cron Job в нашем
предыдущем решении.
удастся для одного из пользователей (OCC обеспечивает, что только одна запись успешна), и мы выдаем автоматический возврат через платежную систему для неудавшегося бронирования. Установите TTL достаточно большим, чтобы минимизировать вероятность этого, и, еще лучше, рассмотрите продление блокировки при инициации оплаты.
Теперь, когда пользователь хочет забронировать билет:
- Пользователь выбирает место на интерактивной карте мест. Клиент делает POST запрос на
/bookingsсticketId, связанными с этим местом. - API-шлюз маршрутизирует запрос в сервис бронирований.
- Сервис бронирований заблокирует этот билет, используя распределенную блокировку на Redis с TTL 10 минут (столько мы будем держать билет).
- Сервис бронирований также создаст новую запись бронирования в базе данных со статусом "in_progress".
- Мы ответим пользователю только что созданным
bookingIdи перенаправим его на страницу оплаты.- Если пользователь остановится здесь, то через 10 минут блокировка автоматически освободится, и билет станет доступен для покупки другим пользователям.
- Пользователь производит оплату на сайте платежной системы. Платежная система обрабатывает платеж и уведомляет нас через webhook об успешной оплате.
- После подтверждения успешной оплаты от платежной системы webhook нашей системы получает
bookingId, встроенный в метаданные платежа. С этимbookingIdwebhook инициирует транзакцию в базе данных для одновременного обновления таблицTicketsиBookings. Конкретно, статус билета, связанного с бронированием, меняется на "sold" в таблицеTickets. Одновременно соответствующая запись бронирования в таблицеBookingsпомечается как "completed". Обработчик webhook должен быть идемпотентным - платежная система может повторять вызовы webhook при сбое, поэтому обработка одного и того же события оплаты дважды не должна приводить к дублированию изменений состояния. ИспользованиеbookingIdкак ключа идемпотентности и проверка текущего статуса бронирования перед обновлением обеспечивает безопасные повторения. - Теперь билет забронирован.
2. Как обработать десятки миллионов одновременных просмотров для популярных мероприятий?
В наших нефункциональных требованиях мы упомянули, что просмотр и поиск мероприятий должны быть высокодоступными, включая сценарии всплеска трафика. Для этого нам потребуется комбинация балансировки нагрузки, горизонтального масштабирования и кэширования.
Страницы мероприятий получают огромную нагрузку, когда билеты
поступают в продажу - тысячи пользователей обновляют одну и ту же
страницу мероприятия одновременно. Эта экстремальная нагрузка на
чтение делает масштабирование чтения критичным и реализуется
через агрессивное кэширование деталей мероприятий, информации о
площадках и схем мест.
Подход Кэширование: 1. Настройте триггеры базы данных для уведомления системы кэширования об изменениях данных, таких как обновления дат мероприятий или состава исполнителей, для инвалидации соответствующих записей кэша. 2. Реализуйте политику TTL для записей кэша, обеспечивая периодическое обновление. Эти TTL могут быть длинными для статичных данных, таких как информация о площадках, и короткими для часто обновляемых данных, таких как доступность билетов на мероприятия. Балансировка нагрузки: Горизонтальное масштабирование: ПроблемыОтличное решение: Кэширование и балансировка нагрузки
3. Как обеспечить хороший пользовательский опыт во время мероприятий с высоким спросом с миллионами одновременных бронирований?
На популярных мероприятиях загруженная карта мест быстро устаревает. Пользователи будут расстраиваться, когда снова и снова нажимают на место, только чтобы узнать, что оно уже забронировано. Нам нужно обеспечить, чтобы карта мест всегда была актуальной и пользователи уведомлялись об изменениях в реальном времени.
Иногда лучшее решение - не самое технически сложное.
Отличительная черта Senior/Staff инженера - это способность
решать бизнес-проблемы, иногда мысля вне предполагаемых
ограничений. Нижеприведенные хорошее и отличное решения
иллюстрируют разницу между Senior и Staff кандидатами.
Подход Чтобы обеспечить актуальность карты мест, можем использовать Server-Sent Events (SSE) для отправки обновлений клиенту в реальном времени. Это позволит обновлять карту мест, как только место забронировано (или зарезервировано) другим пользователем, без необходимости обновления страницы. SSE - это односторонний Проблемы Хотя этот подход хорошо работает для умеренно популярных мероприятий, пользовательский опыт все еще пострадает при экстремально популярных мероприятиях. В случае "проблемы ТейлорХорошее решение: SSE для обновления мест в realtime
канал связи между сервером и клиентом. Он позволяет серверу отправлять данные клиенту без необходимости запроса со стороны клиента.
Свифт" карта мест заполнится сразу и пользователи окажутся в дезориентированном и ошеломленном состоянии, когда доступные места исчезнут моментально.
Подход Для экстремально популярных мероприятий мы можем реализовать управляемую администратором систему виртуальной очереди ожидания для управления доступом пользователей во время исключительно высокого спроса. Пользователи размещаются в 1. Когда пользователь запрашивает просмотр страницы бронирования, он помещается в виртуальную очередь. Мы устанавливаем постоянное соединение (SSE или WebSocket) с клиентом и добавляем его в очередь. Сама очередь может быть реализована на Redis (используя Sorted Sets с метками времени для упорядочивания). SSE проще, поскольку нам нужна только односторонняя связь сервер-клиент для обновлений позиции, хотя WebSocket подойдет, если ожидается двусторонняя связь. 2. Периодически или по определенным критериям (например, какие-то билеты были забронированы) мы извлекаем пользователей из начала очереди и уведомляем их через их соединение, что они могут перейти к покупке билетов. 3. Одновременно помечаем пользователя как "активного" в Redis (например, добавляем их ID сессии во множество Проблемы Долгое время ожидания в очереди может привести к разочарованию пользователей, особенно если предполагаемое время ожидания неточно или очередь движется медленно. Отправляя обновления клиенту в реальном времени, мы можем снизить этот риск, предоставляя пользователям постоянную обратную связь об их позиции вОтличное решение: Виртуальная очередь ожидания
этой очереди до того, как смогут увидеть страницу бронирования с актуальной картой мест. Очередь находится перед сервисом бронирования, контролируя поток пользователей, получающих доступ к интерфейсу бронирования, тем самым предотвращая перегрузку системы и улучшая пользовательский опыт. Вот как это работает на высоком уровне:active:{eventId} с TTL). Сервис бронирования проверяет это множество перед разрешением любых запросов на бронирование, отклоняя пользователей, не прошедших через очередь.
очереди и предполагаемом времени ожидания.
Хотя мы бы не стали использовать SSE для этого случая, многие системы включают какой-то аспект отправки обновлений в реальном времени клиенту. Мы описали все подходы в паттерне Обновления в реальном времени.
4. Как обеспечить быстрый поиск мероприятий?
Наша текущая реализация поиска не справится. Запросы на поиск мероприятий по ключевым словам в названии, описании или других полях потребуют полного сканирования таблицы для оператора LIKE. Это может быть очень медленно, особенно с ростом количества мероприятий.
-- медленный запрос
SELECT *
FROM Events
WHERE name LIKE '%Тейлор%'
OR description LIKE '%Тейлор%'
Давайте рассмотрим некоторые стратегии для улучшения производительности поиска и обеспечения наших требований низкой задержки.
Подход ПроблемыХорошее решение: Индексация и оптимизация SQL-запросов
Events, Performers и Venues для улучшения производительности запросов. Индексы позволяют быстрее извлекать данные, уменьшая количество строк для сканирования. Нужно индексировать колонки, часто используемые в поисковых запросах, такие как название мероприятия, дата мероприятия, имя исполнителя и местоположение площадки.SELECT *, использование LIMIT для ограничения количества возвращаемых строк. Дополнительно использование UNION вместо OR для объединения нескольких запросов иногда может улучшить производительность.
Подход Мы можем расширить базовую стратегию индексации описанную выше для использования полнотекстовых индексов в нашей базе данных, если они доступны. PostgreSQL имеет встроенный полнотекстовый поиск с использованием ПроблемыОтличное решение: Полнотекстовые индексы в базе данных
tsvector и GIN индексов, а MySQL предлагает свой полнотекстовый поиск. Ни один из них не использует Lucene, на котором базируется Elasticsearch. Они делают запросы для конкретных строк вроде "Тейлор" или "Свифт" намного быстрее, чем полное сканирование таблицы с помощью LIKE.
Подход Elasticsearch - мощная поисковая система, превосходно справляющаяся с полнотекстовым поиском, выполнением сложных запросов и обработкой объемного трафика. В своей основе Elasticsearch использует инвертированные индексы - ключевая особенность, делающая его высокоэффективным для поисковых операций. Инвертированные индексы ПроблемыОтличное решение: Полнотекстовая поисковая система
сопоставляют каждое уникальное слово с документами или записями, в которых оно встречается, что значительно ускоряет поисковые запросы.
5. Как ускорить часто повторяющиеся поисковые запросы и снизить нагрузку на поисковую инфраструктуру?
Подход Мы можем использовать механизмы кэширования, такие как Redis или Memcached для хранения результатов часто выполняемых поисковых запросов. Это снижает нагрузку на поисковую инфраструктуру путем обслуживания повторяющихся запросов из кэша Например, запись кэша может выглядеть так: ПроблемыХорошее решение: Стратегии кэширования с Redis
вместо многократного обращения к базе данных или поисковой системе.
{
"key": "search:keyword=Тейлор&start=2021-01-01&end=2021-12-31",
"value": [event1, event2, event3],
"ttl": 60 * 60 * 24 // 24 часа
}
Подход К нашему удобству, Elasticsearch имеет встроенные возможности кэширования, которые можно использовать для хранения результатов частых запросов. Это снижает нагрузку на обработку запросов самой поисковой системы. Elasticsearch поддерживает кэши запросов на уровне шардов для результатов фильтров, плюс отдельный кэш запросов для кэширования полных поисковых ответов, что особенно полезно для запросов с агрегацией. Это можно использовать для адаптивных Также можем использовать CDN для кэширования результатов поиска географически ближе к пользователю, снижая задержку и улучшая время ответа. Заметьте, это имеет смысл только если результаты поиска не персонализированы, то есть один и тот же поисковый запрос возвращает одни и те же результаты для всех пользователей. ПроблемыОтличное решение: Кэширование результатов запросов и CDN
стратегий кэширования, когда система обучается со временем и кэширует результаты наиболее часто выполняемых запросов.
По мере прохождения детальных разборов вы должны обновлять дизайн для отражения вносимых изменений. Итоговый дизайн может выглядеть примерно так:
Визуальная коммуникация важна. Ваш интервьюер занят. Скорее
всего, он завершит собеседование, перейдет к списку встреч,
длящемуся до конца дня и усталый вернется домой, а на следующее
утро вспомнит, что нужно написать отзыв о проведенном вчера
собеседовании. Затем он откроет ваш дизайн и попытается
вспомнить, что вы сказали. Облегчите ему жизнь и улучшите свои
шансы, сделав визуальный дизайн максимально ясным.
Что ожидается на каждом уровне?
Хорошо, мы обсудили много всего. Возникает резонный вопрос: "сколько из этого реально ожидается от меня на интервью?" Разберем по уровням.
Middle
Ширина vs глубина: от Middle кандидата чаще ожидается ширина кругозора и знаний (примерно 80% vs 20%). Вы должны собрать понятный высокоуровневый дизайн, закрывающий все функциональные требования, но многие компоненты могут оставаться абстракциями, которые вы проработали и обсудили с интервьюером на поверхностном
уровне.
Проверка базовых знаний: интервьюер будет прощупывать базу, чтобы удостовериться, что вы понимаете, что делает каждый компонент. Например, добавив API-шлюз, ожидайте вопрос "что он делает" и "как работает".
Смешанный формат ведения: вы должны уверенно вести ранние стадии интервью, но не обязательно проактивно находить все проблемы дизайна. Нормально, если позже интервьюер будет вести обсуждение, задавая вопросы и ставя дополнительные задачи.
Задача Ticketmaster:от Middle кандидата ожидается четко определенный API и модель данных, а также высокоуровневый дизайн покрывающий функциональные требования: просмотр и бронирования мероприятий. Кандидат должен быть способен решить проблему "двойных бронирований" как минимум "хорошим решением" с полем статуса, таймаутом и Cron Job.
Senior
Глубина экспертизы: от Senior кандидата ожидания смещаются к глубине - примерно 60% ширины и 40% глубины. Нужно уметь уходить в детали там, где у вас есть практический опыт. Критично продемонстрировать глубокое понимание ключевых концепций и технологий, релевантных задаче.
Продвинутый дизайн системы: вы должны быть знакомы с продвинутыми принципами проектирования систем. Например, необходимо знание того, как использовать оптимизированное для поиска хранилище данных вроде Elasticsearch для поиска мероприятий. Также ожидается понимание использования распределенной блокировки для резервирования билетов и обсуждение детальных стратегий масштабирования (допустимо, если для этого потребовались подсказки от интервьюера), включая шардирование и репликацию.
Аргументация решений: вы должны уметь ясно объяснять плюсы/минусы архитектурных решений и их влияние на масштабирование, производительность и поддерживаемость, проговаривая компромиссы.
Проактивность и решение проблем: вы должны продемонстрировать сильные навыки решения проблем и проактивный подход. Это подразумевает обнаружение потенциальных проблем в ваших проектах и предложение улучшений. Вам необходимо уметь выявлять и устранять узкие места, оптимизировать производительность и обеспечивать надежность системы.
Задача Ticketmaster: от Senior кандидата ожидается, что вы быстро пройдете высокоуровневый дизайн и потратите время на детальное обсуждение оптимизации поиска, обработки "двойных бронирований" (приходя к распределенной блокировке или другому качественному решению) и даже обсуждение обработки популярных мероприятий, демонстрируя глубину экспертизы в управлении масштабируемостью и надежностью при высокой нагрузке.
Staff+
Акцент на глубину: от Staff+ кандидата ожидается глубокий разбор нюансов - примерно 40% ширины и 60% глубины. Важна демонстрация того, что, даже если вы не решали именно эту задачу раньше, вы решали достаточно похожих задач в реальном
мире, чтобы уверенно спроектировать решение, опираясь на опыт.
Интервьюер понимает, что вы знаете основы (REST, нормализация данных и т. п.), так что вы можете быстро пройти это на high-level дизайне и перейти к самому интересному.
Высокая проактивность: на этом уровне ожидается, что вы будете
самостоятельно выявлять и решать проблемы. Это предполагает не только реагирование на проблемы по мере их возникновения, но и их прогнозирование и реализацию упреждающих решений.
Практическое применение технологий: важно уметь говорить о применяемых технологиях не только в теории, но и как это делается на практике - конфигурации, эксплуатационные нюансы, типичные проблемы.
Решение проблем: ожидаются сильные навыки решения проблем с учетом факторов масштабирования, производительности, надежности и поддерживаемости.
Задача Ticketmaster: от Staff+ кандидата ожидается высокое качество решений по сложным проблемам, которые обсуждались выше. Хорошие кандидаты глубоко погружаются как минимум в 2-3 ключевых области, демонстрируя не только профессионализм, но и инновационное мышление и способности находить оптимальные решения. Хорошим показателем вашей экспертизы является то, что интервьюер
завершает дискуссию, обретя новое понимание или точку зрения.









Top comments (0)