> Как справляться с коллизиями при генерации коротких ссылок (Python)
Уровень: senior · Роль: backend · Язык: Python · Категория: Технические вопросы
Компании: Международный аэропорт Шереметьево
Стек: Python
> Пример ответа
Короткий ответ
Коллизии при генерации коротких ссылок решаются комбинацией трёх подходов: использование достаточно большого пространства идентификаторов (например, base62 от 7+ символов), проверка уникальности перед вставкой с retry-логикой при коллизии, и, при необходимости, переход на суффиксный или итеративный вариант генерации. Ключевой trade-off - между длиной ссылки, вероятностью коллизии и стоимостью проверки. Для высоконагруженных систем добавляют кэширование проверок и предгенерацию пулов идентификаторов.
Подробное объяснение
Коллизия возникает, когда два разных длинных URL получают одинаковый короткий код. Это ломает инвариант "один код - один URL". Основные стратегии:
-
Увеличение пространства ключей - base62 (a-z, A-Z, 0-9) даёт 62^n комбинаций. При n=7 это ~3.5 триллиона. Вероятность коллизии при случайной генерации оценивается через birthday paradox: для 10 млн ссылок при n=7 вероятность коллизии ~1.4%. При n=8 - ~0.005%.
-
Проверка перед вставкой - генерируем код, пытаемся вставить в БД с уникальным индексом. При violation - генерируем заново. Это надёжно, но добавляет latency и нагрузку на БД при высокой конкуренции.
-
Детерминированная генерация - используем хэш от исходного URL (MD5, SHA-256) и берём первые N байт. Коллизии возможны, но при одинаковом входе всегда одинаковый выход. Проблема - предсказуемость и возможность подбора.
-
Счётчик + кодирование - глобальный атомарный счётчик (например, Redis INCR), значение кодируется в base62. Коллизии исключены, но ссылки предсказуемы и есть единая точка отказа.
-
Суффиксный подход - при коллизии добавляем случайный суффикс или увеличиваем длину. Например,
abc123→abc123x. Это компромисс между простотой и надёжностью.
На практике чаще всего комбинируют: генерируют случайный код из достаточно большого пространства, проверяют уникальность, при коллизии - retry (обычно до 3-5 попыток), затем увеличивают длину. Для систем с очень высокой нагрузкой используют предгенерацию пулов кодов в памяти.
На практике
Для senior-позиции важно не просто назвать алгоритм, а обсудить:
- Выбор длины кода - исходя из ожидаемого количества ссылок и допустимой вероятности коллизии. Формула: n = ceil(log_base(Total * k / -ln(1-p))), где p - допустимая вероятность.
- База данных - уникальный индекс на колонку кода обязателен. Использование
INSERT ... ON CONFLICT DO NOTHINGв PostgreSQL или аналогов. - Retry-логика - ограниченное число попыток, экспоненциальный backoff, логирование частоты коллизий как метрики.
- Кэширование - проверка существования кода в Redis перед обращением к БД, но с учётом race condition.
- Масштабирование - при шардировании БД уникальность индекса должна сохраняться на уровне всего кластера.
- Безопасность - не использовать последовательные коды для чувствительных данных, так как они угадываемы.
Пример кода
PYTHONimport secretsimport stringfrom sqlalchemy.exc import IntegrityErrorALPHABET = string.ascii_letters + string.digits # base62BASE = len(ALPHABET)MAX_RETRIES = 5def generate_code(length: int = 7) -> str:"""Криптографически безопасная случайная строка base62."""return ''.join(secrets.choice(ALPHABET) for _ in range(length))def create_short_link(db_session, original_url: str) -> str:for attempt in range(MAX_RETRIES):code = generate_code()try:db_session.add(ShortLink(code=code, url=original_url))db_session.commit()return codeexcept IntegrityError:db_session.rollback()# при последней попытке увеличиваем длинуif attempt == MAX_RETRIES - 1:code = generate_code(length=8)db_session.add(ShortLink(code=code, url=original_url))db_session.commit()return coderaise RuntimeError("Failed to generate unique code")
Альтернатива с предгенерацией пула:
PYTHONdef pregenerate_pool(size: int = 1000) -> list[str]:"""Генерируем пул кодов заранее, помечаем как available."""codes = [generate_code() for _ in range(size)]# batch insert в отдельную таблицу с уникальным индексомreturn codesdef get_available_code(db_session) -> str:# атомарно выбираем и помечаем код как usedcode = db_session.execute(text("SELECT code FROM code_pool WHERE used = false LIMIT 1 FOR UPDATE SKIP LOCKED")).scalar()if not code:raise PoolExhausted()db_session.execute(text("UPDATE code_pool SET used = true WHERE code = :code"), {"code": code})db_session.commit()return code
Как отвечать на собеседовании
Начните с чёткого определения проблемы: коллизия - это нарушение уникальности кода. Затем структурируйте ответ:
- Опишите базовые подходы (случайная генерация, счётчик, хэш) и их trade-off.
- Объясните, почему случайная генерация с проверкой - самый распространённый вариант.
- Упомяните математическую оценку вероятности коллизии - это показывает глубину.
- Перейдите к практическим деталям: уникальный индекс, retry, кэш, метрики.
- Если спросят про масштабирование - обсудите предгенерацию пулов и шардирование.
Хороший тон - задать уточняющий вопрос: "Какое ожидаемое количество ссылок и требования к длине?" Это демонстрирует системное мышление.
Что проверяет интервьюер
- Понимание trade-off между длиной кода и вероятностью коллизии.
- Знание работы с БД (уникальные индексы, обработка IntegrityError).
- Умение проектировать отказоустойчивые алгоритмы (retry, backoff).
- Осведомлённость о проблемах масштабирования и безопасности.
- Способность обсуждать альтернативы и обосновывать выбор.
Типичные ошибки
- Предложение только увеличить длину кода без анализа вероятности.
- Игнорирование уникального индекса в БД - полагание только на проверку в коде (race condition).
- Бесконечный retry без ограничения - деградация под нагрузкой.
- Использование предсказуемых кодов (счётчик) без учёта безопасности.
- Отсутствие метрик на частоту коллизий - нельзя вовремя заметить проблему.
- Неучёт того, что при шардировании уникальность индекса должна быть глобальной.
> Похожие задачи по Python
Какая должна быть длина короткой ссылки, чтобы избежать коллизий при заданной нагрузке и времени жизни
Как оценить необходимое дисковое пространство для хранения сокращенных ссылок с учетом нагрузки и времени жизни
Как хранить маппинг между длинным и коротким URL
Как реализовать функцию сокращения URL
> Похожие задачи по backend
Какая должна быть длина короткой ссылки, чтобы избежать коллизий при заданной нагрузке и времени жизни
Как оценить необходимое дисковое пространство для хранения сокращенных ссылок с учетом нагрузки и времени жизни
Как хранить маппинг между длинным и коротким URL
Как реализовать функцию сокращения URL
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью