Кейс: сократитель ссылок (URL shortener)

Классическая задача собеседования: спроектировать сервис вроде bit.ly, превращающий длинный URL в короткий.

Идём строго по шаблону: требования → оценки → высокоуровневая схема → детали → узкие места. Этот же каркас подойдёт почти к любой задаче.

1. Требования

Функциональные:

  • по длинному URL создать короткий (например, short.ly/abc123);
  • переход по короткому → редирект на длинный;
  • (опц.) пользовательский алиас и срок жизни ссылки;
  • (опц.) аналитика кликов.

Нефункциональные: редирект быстрый (p99 < 100 мс); высокая доступность (битая ссылка хуже медленной); чтений ≫ записей; ссылки не должны теряться и коллизий быть не должно.

2. Оценки масштаба

Пусть 100 млн новых ссылок в месяц. Соотношение чтений к записям возьмём 100:1.

Записи: 100 млн / мес ≈ 40 записей/с (100M / 2.5M сек)
Чтения: 40 * 100 = 4000 редиректов/с
Пик чтения: ~ x2 = ~8000 QPS
Хранилище: ~500 байт на ссылку * 100M * 12 мес * 5 лет
         ≈ 0.5KB * 6 млрд ≈ 3 TB

Выводы сразу: 3 TB на годы — поместится на нескольких узлах; 8000 QPS на чтение — нужен кэш; чтений в 100 раз больше — оптимизируем именно редирект.

3. Сколько символов в коде

Используем алфавит из 62 символов (a–z, A–Z, 0–9). Сколько ссылок закодируем кодом длины n? 62ⁿ.

Длина кодаСколько уникальных ссылок
6 символов62⁶ ≈ 56 млрд
7 символов62⁷ ≈ 3,5 трлн

За 5 лет нам нужно ~6 млрд кодов — 7 символов дают огромный запас. Берём 7.

4. Высокоуровневая схема

[клиент] → [LB] → [сервис ссылок] → [кэш Redis] → [БД код→URL]
                       создание: сгенерировать код, записать в БД
                       редирект: код → URL (сначала кэш, потом БД) → 301

5. Как генерировать код

ПодходПлюсМинус
Хеш URL (MD5) → первые 7 символовдетерминированколлизии, надо проверять/разрешать
Счётчик + base62без коллизий, короткокоды предсказуемы, счётчик — узкое место
Случайный код + проверка уникальностинепредсказуемлишняя проверка в БД

Хорошее решение — счётчик с base62: глобальный счётчик (или диапазоны, раздаваемые узлам, чтобы он не стал SPOF) даёт уникальное число, его кодируем в 7 символов. Диапазоны (например, по 1000 id на узел) убирают узкое место единого счётчика.

6. Модель данных и редирект

{
  "short_code": "a9Bx2Cd",
  "long_url": "https://example.com/very/long/path",
  "created_at": 1718380800,
  "expires_at": null
}

Доступ всегда по ключу short_code — идеально для key-value хранилища и кэша. Редирект отдаём как HTTP 301/302. Нюанс: 301 (постоянный) браузеры кэшируют — быстро, но клики хуже считаются; 302 (временный) всегда идёт через сервис — нужно для аналитики. Это компромисс «скорость против учёта».

7. Узкие места и решения

Узкое местоРешение
8000 QPS на чтениекэш горячих ссылок (Redis), hit rate высок — ссылки популярны неравномерно
3 TB данныхшардирование по short_code (hash/range)
Единый счётчик — SPOFраздавать узлам диапазоны id
Доступность редиректареплики БД, кэш переживает падение БД

Итог

  • Шаблон работает: требования → оценки → схема → детали → узкие места.
  • 7 символов base62 (62⁷ ≈ 3,5 трлн) с запасом покрывают объём; счётчик с диапазонами убирает коллизии и SPOF.
  • Чтений ≫ записей → главное оптимизировать редирект: кэш + шардирование + реплики.
Проверьте себя
1. Почему для кодов выбрали 7 символов base62, а не 6?
A7 короче и красивее
B6 символов технически невозможны
CТак требует HTTP
D62⁷ ≈ 3,5 трлн — это с большим запасом покрывает нужные ~6 млрд ссылок за 5 лет
2. Что в первую очередь оптимизируют в сократителе ссылок и почему?
AРедирект (чтение), потому что чтений примерно в 100 раз больше, чем записей
BЗапись, потому что ссылок создаётся очень много
CУдаление ссылок
DИнтерфейс администратора
3. Чем плох единый глобальный счётчик для генерации кодов и как это лечат?
AОн слишком длинный; лечат сокращением
BОн становится узким местом и SPOF; лечат раздачей узлам диапазонов id
CОн даёт коллизии; лечат хешированием
DНичем не плох
4. В чём компромисс между редиректом 301 и 302?
A301 медленнее 302
B302 нельзя использовать для редиректа
C301 кэшируется браузером (быстро, но хуже учёт кликов), 302 всегда идёт через сервис (учёт, но без кэша)
DМежду ними нет разницы