Кейс: сократитель ссылок (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 короче и красивее
B62⁷ ≈ 3,5 трлн — это с большим запасом покрывает нужные ~6 млрд ссылок за 5 лет
C6 символов технически невозможны
DТак требует HTTP
2. Что в первую очередь оптимизируют в сократителе ссылок и почему?
AЗапись, потому что ссылок создаётся очень много
BРедирект (чтение), потому что чтений примерно в 100 раз больше, чем записей
CУдаление ссылок
DИнтерфейс администратора
3. Чем плох единый глобальный счётчик для генерации кодов и как это лечат?
AОн слишком длинный; лечат сокращением
BОн становится узким местом и SPOF; лечат раздачей узлам диапазонов id
CОн даёт коллизии; лечат хешированием
DНичем не плох
4. В чём компромисс между редиректом 301 и 302?
A301 медленнее 302
B301 кэшируется браузером (быстро, но хуже учёт кликов), 302 всегда идёт через сервис (учёт, но без кэша)
C302 нельзя использовать для редиректа
DМежду ними нет разницы
Поддержать проект