系统设计:URL短链服务

从零设计一个支持每日10亿次请求的URL短链服务,详解哈希算法、发号器策略、缓存架构与数据库设计,包含完整架构图与代码实现。

系统设计:URL 短链服务

设计类似 bit.ly 的短链服务,支持:

  • 缩短长 URL
  • 根据短码重定向到原始 URL
  • 高可用、低延迟(< 100ms)

1. 需求分析

功能性需求

  • 短链生成:输入长 URL,返回短码
  • 重定向:短码 301/302 跳转至长 URL
  • 自定义短码(可选)
  • 失效时间(可选)

非功能性需求

  • 高可用:99.99%
  • 低延迟:读写 < 100ms
  • 高并发:10 亿次/日 ≈ 11,574 QPS,峰值 100,000 QPS
  • 短码长度:6-8 字符(62 进制:a-zA-Z0-9)

2. 容量估算

  • 日新增:1000 万条
  • 年新增:36.5 亿条
  • 存储:每条 ≈ 500 字节(短码 + 长 URL + 元数据)
  • 年存储:3650 万 × 500B ≈ 18.25 GB

3. 数据库设计

CREATE TABLE urls (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    short_code VARCHAR(8) UNIQUE NOT NULL,
    long_url VARCHAR(2048) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    expires_at TIMESTAMP NULL,
    click_count BIGINT DEFAULT 0,
    INDEX idx_short_code (short_code)
);

4. 短码生成策略

方案一:Base62 编码(发号器)

import string

BASE62 = string.ascii_letters + string.digits

def encode_base62(num):
    if num == 0:
        return BASE62[0]
    result = []
    while num > 0:
        result.append(BASE62[num % 62])
        num //= 62
    return ''.join(reversed(result))

def decode_base62(s):
    result = 0
    for char in s:
        result = result * 62 + BASE62.index(char)
    return result

发号器:

  • 单机:自增 ID
  • 分布式:Snowflake(避免单点)、号段模式(数据库批量取号)

方案二:MD5 哈希取前 6 位

import hashlib

def short_code_md5(url):
    return hashlib.md5(url.encode()).hexdigest()[:6]

问题:哈希冲突,需处理碰撞。

方案对比

方案优点缺点
发号器无冲突、趋势递增可能预测
哈希分布均匀冲突处理复杂

5. 系统架构

Client → CDN/边缘缓存 → API Gateway → Load Balancer
                                      ↓
                        ┌─────────────┼─────────────┐
                        ↓             ↓             ↓
                     Read Cache    Write API    Analytics
                        ↓             ↓
                     Redis        MySQL (Master-Slave)
                        ↓             ↓
                     Bloom Filter   Sharding

缓存策略

读取优化:

  • 短码 → 长 URL 映射缓存在 Redis(TTL 24h)
  • 命中缓存直接返回,延迟 < 1ms
  • 未命中查数据库并回填缓存

布隆过滤器:

  • 防止缓存穿透
  • 不存在的短码快速返回 404

6. 代码实现

from fastapi import FastAPI, HTTPException, RedirectResponse
import redis
import hashlib

app = FastAPI()
redis_client = redis.Redis(host='localhost', port=6379, db=0)

@app.post("/shorten")
def shorten_url(long_url: str, custom_code: str = None):
    if custom_code:
        short_code = custom_code
    else:
        # 使用哈希 + 冲突处理
        for i in range(10):
            candidate = hashlib.md5(f"{long_url}:{i}".encode()).hexdigest()[:6]
            if not redis_client.exists(candidate):
                short_code = candidate
                break
        else:
            raise HTTPException(500, "无法生成短码")

    redis_client.setex(short_code, 86400, long_url)
    return {"short_url": f"https://short.ly/{short_code}"}

@app.get("/{short_code}")
def redirect(short_code: str):
    long_url = redis_client.get(short_code)
    if not long_url:
        raise HTTPException(404, "短码不存在")
    return RedirectResponse(url=long_url.decode(), status_code=301)

7. 性能优化

策略效果
Redis 缓存读 QPS 提升 1000x
CDN 边缘缓存全球延迟降低至 < 50ms
数据库读写分离写不阻塞读
分片(Sharding)数据库水平扩展

8. 面试常见问题

Q: 301 vs 302 重定向的区别?

  • 301:永久重定向,浏览器缓存,减少后续请求
  • 302:临时重定向,每次都访问短链服务(便于统计)

Q: 如何处理哈希冲突?

  • 开放寻址:尝试下一个槽位
  • 链地址法:同一桶内链表存储
  • 再哈希:换一个哈希函数

Q: 短码长度为什么是 6 位?
62^6 ≈ 568 亿,足够覆盖大部分场景,同时保持 URL 简洁。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页