> Как хранить маппинг между длинным и коротким URL (Python)
Уровень: senior · Роль: backend · Язык: Python · Категория: Технические вопросы
Компании: Международный аэропорт Шереметьево
Стек: Python
> Пример ответа
Короткий ответ
Основной подход - хранить маппинг в key-value хранилище (Redis, DynamoDB, Cassandra) с ключом - коротким идентификатором, значением - длинным URL. Для генерации короткого кода используют base62 от счётчика, случайные строки с проверкой коллизий или хэш с обрезкой. Обязательно закладывают TTL для неиспользуемых записей и кэширование популярных маппингов. Для аналитики и кастомных алиасов добавляют отдельную таблицу с уникальным индексом на короткий код.
Подробное объяснение
Маппинг "короткий → длинный" - это по сути таблица соответствий, где ключом является короткий идентификатор (6-10 символов), а значением - полный URL. Основные требования: быстрый lookup по короткому коду, устойчивость к коллизиям, возможность протухания записей.
Варианты хранения:
-
Реляционная БД (PostgreSQL/MySQL) - таблица
short_urls(id BIGSERIAL, short_code VARCHAR(10) UNIQUE, long_url TEXT, created_at, expires_at). Плюс - простота, транзакции, индексы. Минус - при высоком RPS упирается в лимиты соединений, нужен шардинг. -
Key-value хранилище (Redis/DynamoDB) - ключ
short:{code}→long_url. Redis даёт sub-millisecond latency и встроенный TTL. DynamoDB масштабируется горизонтально, но нет TTL на уровне записи (есть через attribute). -
Гибрид - Redis как горячий кэш перед PostgreSQL/DynamoDB. Это стандартный паттерн: 90% запросов попадают в кэш, остальные идут в основное хранилище.
Генерация короткого кода:
- Счётчик + base62 - атомарно инкрементируем глобальный счётчик (в Redis или в БД), конвертируем число в base62. Детерминированно, нет коллизий, но нужен единый источник счётчика.
- Случайная строка - генерируем N случайных символов из алфавита [a-zA-Z0-9]. Риск коллизий растёт с количеством записей, нужна проверка уникальности и повторная генерация при коллизии.
- Хэш от длинного URL - MD5/SHA256 обрезаем до нужной длины. Детерминированно для одинаковых URL, но коллизии возможны, и злоумышленник может подобрать коллизию.
Ключевые trade-off:
- Длина кода vs количество записей: 6 символов base62 = 56 млрд комбинаций, 7 = 3.5 трлн.
- Детерминированность vs безопасность: счётчик предсказуем, случайные строки - нет.
- TTL vs постоянное хранение: для временных ссылок TTL обязателен, для постоянных - нет.
На практике
Для senior-позиции важно показать понимание не только "как хранить", но и "как масштабировать". Обычно решение выглядит так:
- При создании короткой ссылки: генерируем код, пишем в основное хранилище (PostgreSQL или DynamoDB), параллельно кладём в Redis с TTL.
- При редиректе: сначала Redis, при промахе - основное хранилище, затем заполняем кэш.
- Для аналитики: отдельная таблица/стрим с событиями переходов (не в том же хранилище, чтобы не замедлять lookup).
- Для кастомных алиасов: отдельная таблица с уникальным индексом, проверка при создании.
Важный момент - идемпотентность: если один и тот же длинный URL сокращают дважды, нужно решить, возвращать существующий код или создавать новый. Обычно возвращают существующий, если он не истёк.
Пример кода
PYTHONimport base64import hashlibimport redisimport psycopg2from psycopg2.extras import RealDictCursorclass Shortener:ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"BASE = len(ALPHABET)def __init__(self, redis_client, db_conn):self.redis = redis_clientself.db = db_conndef _encode_base62(self, num: int) -> str:if num == 0:return self.ALPHABET[0]code = []while num > 0:num, rem = divmod(num, self.BASE)code.append(self.ALPHABET[rem])return ''.join(reversed(code))def _generate_code(self, long_url: str) -> str:# вариант 1: хэш от URL (детерминированный)digest = hashlib.sha256(long_url.encode()).digest()# берём 6 байт -> 48 бит -> base62, получаем ~8 символовnum = int.from_bytes(digest[:6], 'big')return self._encode_base62(num)[:8]def create_short_url(self, long_url: str, ttl_seconds: int = 86400) -> str:code = self._generate_code(long_url)# проверяем коллизию в Redisif self.redis.exists(f"short:{code}"):existing = self.redis.get(f"short:{code}")if existing == long_url:return code# коллизия - добавляем случайный суффиксcode = self._generate_code(long_url + str(time.time()))[:8]# пишем в основное хранилищеwith self.db.cursor() as cur:cur.execute("INSERT INTO short_urls (short_code, long_url, expires_at) ""VALUES (%s, %s, NOW() + INTERVAL '%s seconds') ""ON CONFLICT (short_code) DO NOTHING",(code, long_url, ttl_seconds))self.db.commit()# пишем в кэшself.redis.setex(f"short:{code}", ttl_seconds, long_url)return codedef resolve(self, code: str) -> str | None:# сначала кэшlong_url = self.redis.get(f"short:{code}")if long_url:return long_url# промах - идём в БДwith self.db.cursor(cursor_factory=RealDictCursor) as cur:cur.execute("SELECT long_url FROM short_urls ""WHERE short_code = %s AND (expires_at IS NULL OR expires_at > NOW())",(code,))row = cur.fetchone()if row:# заполняем кэшself.redis.setex(f"short:{code}", 3600, row["long_url"])return row["long_url"]return None
Как отвечать на собеседовании
Начни с краткого ответа, затем раскрой архитектуру. Обязательно упомяни:
- Выбор хранилища - обоснуй, почему key-value, а не реляционка, или наоборот. Для senior важно показать, что ты понимаешь разницу в latency, масштабировании и стоимости.
- Генерация кода - сравни подходы, назови trade-off каждого.
- Кэширование - объясни, зачем Redis перед БД, какой TTL ставить, как инвалидировать.
- Масштабирование - что будет при 100k RPS, как шардировать, как решать проблему горячих ключей.
- Надёжность - что при падении Redis, как восстанавливать, нужна ли репликация.
Хороший ответ включает упоминание edge cases: коллизии, истёкшие ссылки, кастомные алиасы, rate limiting на создание.
Что проверяет интервьюер
- Глубина понимания системного дизайна - умеешь ли ты выбирать хранилище под нагрузку, а не по привычке.
- Умение видеть trade-off - не существует идеального решения, важно показать, что ты осознаёшь ограничения каждого варианта.
- Практический опыт - знаешь ли ты реальные проблемы: коллизии, TTL, кэширование, шардирование.
- Навыки проектирования API - как ты будешь обрабатывать создание, редирект, удаление.
- Понимание безопасности - предсказуемость кодов, возможность перебора, rate limiting.
Типичные ошибки
- Предлагать только реляционную БД без обсуждения нагрузки - сразу показывает отсутствие опыта с высоконагруженными системами.
- Игнорировать коллизии - говорить "возьмём хэш и обрежем", не упоминая вероятность коллизий и стратегию их разрешения.
- Не упоминать TTL - забыть, что ссылки должны протухать, иначе хранилище растёт бесконечно.
- Не разделять горячий и холодный путь - хранить всё в одной БД без кэша, не понимая, что 99% запросов - это редиректы.
- Путать генерацию кода и хранение - начинать с алгоритмов генерации, не определившись с хранилищем.
- Не учитывать кастомные алиасы - если пользователь хочет свой короткий код, нужно отдельное решение, а не только автогенерация.
> Похожие задачи по Python
Как оценить необходимое дисковое пространство для хранения сокращенных ссылок с учетом нагрузки и времени жизни
Как справляться с коллизиями при генерации коротких ссылок
Как реализовать функцию сокращения URL
Как проверять корректность работы сервиса и обработку сообщений по разным приоритетам вовремя
> Похожие задачи по backend
Как оценить необходимое дисковое пространство для хранения сокращенных ссылок с учетом нагрузки и времени жизни
Как справляться с коллизиями при генерации коротких ссылок
Как реализовать функцию сокращения URL
Как проверять корректность работы сервиса и обработку сообщений по разным приоритетам вовремя
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью