正在加载,请稍候…

系统设计:构建可扩展的短链接服务

设计类似 bit.ly 的短链接服务。了解哈希策略、数据库设计、缓存层、分析以及如何扩展以处理每天数百万次的重定向。

系统设计:构建可扩展的短链接服务

系统设计:可扩展的短链接服务

需求

功能性:

  • 缩短 URL(自定义或生成别名)
  • 将短 URL 重定向到原始 URL
  • 分析(点击次数、来源、地理位置)
  • 链接过期

非功能性:

  • 每天创建 1 亿个 URL(写入:约 1200/s)
  • 每天 100 亿次重定向(读取:约 115,000/s)—— 100:1 读写比
  • 重定向可用性 99.99%
  • P99 重定向延迟 < 50ms

系统设计:构建可扩展的短链接服务示意图

数据估算

存储:
- 每天 1 亿个新 URL x 365 天 x 5 年 = 1825 亿个 URL
- 每个 URL:约 500 字节(ID + 原始 URL + 元数据)
- 总计:约 90TB

流量:
- 峰值 115,000 次重定向/秒
- 80/20 规则:20% 的 URL 承载 80% 的流量

URL 缩短算法

import string, hashlib

ALPHABET = string.ascii_letters + string.digits  # 62 个字符
BASE = 62

def encode(num: int) -> str:
    result = []
    while num > 0:
        result.append(ALPHABET[num % BASE])
        num //= BASE
    return ''.join(reversed(result)) or ALPHABET[0]

def generate_short_code(url: str, user_id: str) -> str:
    input_str = f"{url}{user_id}"
    hash_value = hashlib.md5(input_str.encode()).hexdigest()
    num = int(hash_value[:8], 16)
    return encode(num).ljust(7, ALPHABET[0])[:7]

系统设计:构建可扩展的短链接服务示意图

数据库设计

CREATE TABLE urls (
  id          BIGSERIAL PRIMARY KEY,
  short_code  VARCHAR(10) UNIQUE NOT NULL,
  long_url    TEXT NOT NULL,
  user_id     BIGINT REFERENCES users(id),
  created_at  TIMESTAMP DEFAULT NOW(),
  expires_at  TIMESTAMP
);
CREATE INDEX idx_short_code ON urls(short_code);

-- 分析(单独的表/服务)
CREATE TABLE url_clicks (
  id          BIGSERIAL PRIMARY KEY,
  short_code  VARCHAR(10) NOT NULL,
  clicked_at  TIMESTAMP DEFAULT NOW(),
  ip_address  INET,
  country     CHAR(2)
);

架构

客户端 -> CDN/负载均衡器 -> API 服务器(无状态)
                                  |
              写入路径                  |                  读取路径
                  |               |              |
         验证服务               |       Redis 缓存(TTL=24h)
                  |               |         命中 -> 返回 URL
           PostgreSQL(主)       |        未命中 -> 读取数据库 -> 缓存
                                  |
分析:API -> Kafka -> ClickHouse(批量聚合)

系统设计:构建可扩展的短链接服务示意图

缓存策略

async def get_long_url(short_code: str) -> str:
    cache_key = f"url:{short_code}"
    cached = await redis.get(cache_key)
    if cached:
        return cached.decode()

    url = await db.fetchone("SELECT long_url FROM urls WHERE short_code = $1", short_code)
    if not url:
        raise NotFoundException(short_code)

    # 抖动 TTL 以防止惊群效应
    import random
    ttl = 86400 + random.randint(0, 3600)
    await redis.setex(cache_key, ttl, url['long_url'])
    return url['long_url']

扩展考虑

  1. 读取扩展:热门链接使用 CDN,Redis 集群作为缓存
  2. 写入扩展:按 short_code 范围分片数据库,预生成代码池
  3. 分析:使用 Kafka + ClickHouse,不拖慢重定向
  4. 地理分布:多区域部署,本地缓存

短链接服务是一个典型的读密集型系统,受益于积极缓存。