← Назад к вопросам
Что такое сервисы сокращения ссылок?
2.3 Middle🔥 171 комментариев
#DevOps и инфраструктура#Django
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Сервисы сокращения ссылок
Сервис сокращения ссылок (URL Shortener) — это веб-приложение, которое преобразует длинные URL в короткие, легко запоминающиеся ссылки. Примеры: bit.ly, tinyurl.com, короткие ссылки в твиттере.
Основные функции
- Сокращение URL: длинная ссылка → короткая
- Редирект: переход по короткой ссылке на оригинальную
- Статистика: отслеживание количества кликов
- Кастомизация: создание ссылок с нужным кодом
Архитектура сервиса сокращения ссылок
from fastapi import FastAPI, HTTPException
from sqlalchemy import Column, String, Integer, DateTime, create_engine
from sqlalchemy.orm import Session, declarative_base
from datetime import datetime
import secrets
import string
app = FastAPI()
Base = declarative_base()
engine = create_engine("postgresql://user:password@localhost/urlshortener")
class URLMapping(Base):
"""Модель для хранения сокращённых ссылок"""
__tablename__ = "url_mappings"
id = Column(Integer, primary_key=True)
short_code = Column(String(10), unique=True, index=True)
original_url = Column(String(2048))
created_at = Column(DateTime, default=datetime.utcnow)
clicks = Column(Integer, default=0)
user_id = Column(Integer) # Для отслеживания кто создал
Base.metadata.create_all(engine)
def generate_short_code(length: int = 6) -> str:
"""Генерирует уникальный короткий код"""
characters = string.ascii_letters + string.digits
return ''.join(secrets.choice(characters) for _ in range(length))
@app.post("/api/v1/shorten")
async def shorten_url(original_url: str, custom_code: str = None, db: Session = None):
"""API для сокращения ссылки"""
# Генерируем уникальный код
if custom_code:
existing = db.query(URLMapping).filter_by(short_code=custom_code).first()
if existing:
raise HTTPException(status_code=400, detail="Code already taken")
short_code = custom_code
else:
while True:
short_code = generate_short_code()
existing = db.query(URLMapping).filter_by(short_code=short_code).first()
if not existing:
break
# Сохраняем в БД
mapping = URLMapping(
short_code=short_code,
original_url=original_url
)
db.add(mapping)
db.commit()
return {
"short_url": f"https://short.url/{short_code}",
"original_url": original_url
}
@app.get("/{short_code}")
async def redirect_url(short_code: str, db: Session = None):
"""Редирект с отслеживанием"""
# Находим оригинальную ссылку
mapping = db.query(URLMapping).filter_by(short_code=short_code).first()
if not mapping:
raise HTTPException(status_code=404, detail="URL not found")
# Увеличиваем счётчик кликов
mapping.clicks += 1
db.commit()
# Редиректим пользователя
from fastapi.responses import RedirectResponse
return RedirectResponse(url=mapping.original_url)
@app.get("/api/v1/stats/{short_code}")
async def get_stats(short_code: str, db: Session = None):
"""Получить статистику по ссылке"""
mapping = db.query(URLMapping).filter_by(short_code=short_code).first()
if not mapping:
raise HTTPException(status_code=404)
return {
"short_code": mapping.short_code,
"original_url": mapping.original_url,
"clicks": mapping.clicks,
"created_at": mapping.created_at
}
Оптимизация: кэширование
Для популярных ссылок кэшируем редиректы:
import redis
redis_client = redis.Redis(host="localhost", port=6379, db=0)
CACHE_TTL = 3600 # 1 час
@app.get("/{short_code}")
async def redirect_url_cached(short_code: str, db: Session = None):
"""Редирект с кэшированием"""
# Сначала проверяем Redis
cached_url = redis_client.get(f"url:{short_code}")
if cached_url:
# Кэш найден, но считаем клики в БД
mapping = db.query(URLMapping).filter_by(short_code=short_code).first()
if mapping:
mapping.clicks += 1
db.commit()
return RedirectResponse(url=cached_url.decode())
# Если нет в кэше, ищем в БД
mapping = db.query(URLMapping).filter_by(short_code=short_code).first()
if not mapping:
raise HTTPException(status_code=404)
# Сохраняем в кэш
redis_client.setex(f"url:{short_code}", CACHE_TTL, mapping.original_url)
# Увеличиваем счётчик
mapping.clicks += 1
db.commit()
return RedirectResponse(url=mapping.original_url)
Масштабирование: кэширование кликов
import asyncio
# Вместо обновления БД при каждом клике, батчим обновления
click_cache = {} # {short_code: count}
async def update_clicks_in_background():
"""Периодически обновляет клики в БД"""
while True:
await asyncio.sleep(5) # Обновляем каждые 5 секунд
if not click_cache:
continue
db = SessionLocal()
for short_code, count in click_cache.items():
mapping = db.query(URLMapping).filter_by(short_code=short_code).first()
if mapping:
mapping.clicks += count
db.commit()
db.close()
click_cache.clear()
@app.get("/{short_code}")
async def redirect_url_batched(short_code: str):
"""Батчинг кликов"""
cached_url = redis_client.get(f"url:{short_code}")
if not cached_url:
# Логика получения из БД
pass
# Увеличиваем счётчик в памяти
click_cache[short_code] = click_cache.get(short_code, 0) + 1
return RedirectResponse(url=cached_url.decode())
Обработка уникальности кодов
import hashlib
import base64
def generate_short_code_from_url(url: str) -> str:
"""Детерминированное сокращение на основе URL"""
hash_obj = hashlib.md5(url.encode())
hash_bytes = hash_obj.digest()[:6] # Берём первые 6 байт
return base64.urlsafe_b64encode(hash_bytes).decode()[:6]
# Проблема: коллизии при длинных URL
# Решение: если код занят, добавляем суффикс
def get_unique_code(original_url: str, db: Session) -> str:
"""Получить уникальный код"""
base_code = generate_short_code_from_url(original_url)
code = base_code
counter = 0
while db.query(URLMapping).filter_by(short_code=code).first():
counter += 1
code = f"{base_code}{counter}"
return code
Проблемы при масштабировании
1. Высокая нагрузка на редиректы
# Решение: использовать CDN для часто запрашиваемых ссылок
from cloudflare import Cloudflare
cdn = Cloudflare(api_token="token")
def purge_cdn_cache(short_code: str):
"""Обновить кэш CDN"""
cdn.purge(f"https://short.url/{short_code}")
2. Растущая БД
# Решение: партиционирование по времени создания
# или использование sharding по хешу short_code
# Пример: распределение записей по нескольким БД
SHARD_COUNT = 10
def get_shard(short_code: str) -> int:
return hash(short_code) % SHARD_COUNT
def save_url(original_url: str, custom_code: str):
shard_id = get_shard(custom_code)
db = get_db_connection(shard_id)
# Сохраняем в нужную шарду
3. Одновременные запросы
# Решение: использовать Redis для генерации уникальных ID
class DistributedSequence:
def __init__(self, redis_client):
self.redis = redis_client
def next_code(self) -> str:
"""Генерирует уникальный ID через Redis"""
counter = self.redis.incr("url_counter")
# Преобразуем число в буквы (base62)
return self.to_base62(counter)
@staticmethod
def to_base62(num: int) -> str:
chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
if num == 0:
return chars[0]
arr = []
while num:
arr.append(chars[num % 62])
num //= 62
return ''.join(reversed(arr))
Полный пример с быстрым редиректом
from fastapi import FastAPI
from fastapi.responses import RedirectResponse
import redis
from sqlalchemy import create_engine, Column, String, Integer, DateTime
from sqlalchemy.orm import Session
from datetime import datetime
app = FastAPI()
redis_client = redis.Redis()
class URLMapping(Base):
__tablename__ = "url_mappings"
short_code = Column(String(10), primary_key=True, index=True)
original_url = Column(String(2048))
created_at = Column(DateTime, default=datetime.utcnow)
clicks = Column(Integer, default=0)
@app.get("/api/v1/shorten")
def shorten(url: str):
code = generate_code()
db.add(URLMapping(short_code=code, original_url=url))
db.commit()
return {"short_code": code}
@app.get("/{code}")
def redirect(code: str):
# Кэш в Redis
cached = redis_client.get(code)
if cached:
url = cached.decode()
else:
mapping = db.query(URLMapping).get(code)
if not mapping:
return {"error": "Not found"}, 404
url = mapping.original_url
redis_client.setex(code, 3600, url)
return RedirectResponse(url=url)
Сервис сокращения ссылок — классический пример системного дизайна, требующий масштабируемости, надёжности и производительности.