Как хранить маппинг между длинным и коротким URL
Что проверяют
Это маленькая задача системного дизайна: где хранить соответствие кода и адреса, как генерировать код без коллизий и как выдержать нагрузку, где чтений намного больше, чем записей. Следом спрашивают, что будет при нескольких инстансах приложения и нужен ли кеш.
Как отвечать
Словарь в памяти годится только для разминки: он пропадает при рестарте и не общий для воркеров Gunicorn или Uvicorn. Основное хранилище это база.
- таблица:
id bigintиз последовательности,codeс уникальным индексом,long_url text,created_at, при необходимостиexpires_atи владелец; - код по счётчику:
idпереводится в Base62 (цифры и латиница в двух регистрах). Семь символов дают 62 в седьмой степени, примерно 3,5 триллиона кодов. Коллизий нет по построению, но коды идут подряд и легко перебираются, поэтому id перемешивают, например библиотекой Sqids; - случайный код:
secrets.choiceпо алфавиту из 62 символов, вставка, приIntegrityErrorновая попытка. Модульrandomдля этого не подходит, он предсказуем; - хэш от адреса: обрезанный SHA-256 даёт одинаковый код для одинакового адреса, но коллизии всё равно надо проверять;
- дедупликация: если один адрес должен давать один код, уникальный индекс вешают на хэш адреса, а не на сам
text: очень длинная строка в B-tree индекс PostgreSQL не помещается.
Чтение: сначала Redis по ключу кода с TTL, при промахе база и запись в кеш. Редирект со статусом 302 или 307, если нужны счётчики переходов: ответ 301 браузер кеширует и больше к вам не приходит.
Где спотыкаются
Генерируют код как base64 от строки счётчика и обрезают до шести символов. Обрезка ломает уникальность: у счётчиков 100000 и 100001 первые шесть символов совпадают.
Проверяют существование кода отдельным SELECT перед вставкой. Между проверкой и вставкой успевает пройти соседний запрос, от гонки защищает только уникальный индекс.
Забывают про чтение: всё внимание генерации, а кеш, TTL и выбор статуса редиректа не упомянуты, хотя именно там основная нагрузка.