短链系统设计

1.1 需求分析

功能性需求

  • 给定长 URL,生成唯一短链(如 t.cn/xK3m9
  • 访问短链时,重定向到原始长 URL
  • 短链可设置有效期
  • 提供访问统计(点击量、来源、设备)

非功能性需求

  • 低延迟:重定向 < 50ms
  • 高可用:99.99%(重定向链路不能挂)
  • 高并发写入:日均 1 亿条短链生成
  • 短链不可重复,同一长链尽量返回相同短链

1.2 容量估算

假设日生成 1 亿条短链:

1
2
3
4
5
6
7
8
9
10
写入 QPS = 100,000,000 / 86,400 ≈ 1,157 QPS

读取 QPS:假设每条短链平均被点击 10 次
= 1,157 × 10 × 10(历史累积效应)≈ 115,700 QPS

存储:每条记录约 500 Byte(长链 + 短链 + 元数据 + 索引)
日增 = 1 亿 × 500B ≈ 50 GB
年增 = 50 GB × 365 ≈ 18.25 TB

带宽:115,700 × 1KB(302 响应体极小)≈ 115 MB/s ≈ 920 Mbps

结论:系统瓶颈在读,而非写。读取量是写入量的 100 倍。


1.3 短链生成算法

这是短链系统的核心难题。两种主流方案:

方案 A:Hash + 进制转换

1
2
3
4
1. 对长 URL 计算 MD5/SHA256 → 128/256 bit 哈希值
2. 取前 8 字节(64 bit)转为十进制数字
3. 十进制转 Base62 [0-9a-zA-Z] 得到 7 位短码
4. 如果哈希冲突,在长链末尾拼上随机串重新哈希
优点 缺点
不需要发号器,无单点 哈希冲突率随数据量增大而上升
签名算法保证相同长链得到相同短链 无法保证短链长度固定

方案 B:自增 ID + Base62(推荐)

1
2
3
4
1. 发号器生成全局唯一自增 ID(Snowflake 或数据库自增)
2. 将 ID 转为 Base62 编码 → 短链
例:ID=100000000 → Base62 → "6LAze"
3. 用布隆过滤器判重,处理同一长链重复申请

Base62 编码字符集:

1
2
0-9(10) + a-z(26) + A-Z(26) = 62 个字符
7 位可表示 62^73.5 万亿,足够使用

为什么不用 UUID?

  • UUID 长度为 128 bit,Base62 编码后仍有约 22 位,太长了
  • UUID 无序,数据库插入时 B+ 树频繁分裂,性能差

1.4 数据库设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
CREATE TABLE short_url (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
short_code VARCHAR(8) NOT NULL UNIQUE,
long_url TEXT NOT NULL,
long_hash CHAR(32) NOT NULL, -- MD5 前 128 位
expires_at DATETIME DEFAULT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_long_hash (long_hash),
INDEX idx_expires_at (expires_at)
);

CREATE TABLE access_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
short_code VARCHAR(8) NOT NULL,
user_ip VARCHAR(45),
user_agent VARCHAR(512),
referer VARCHAR(1024),
accessed_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_sc_at (short_code, accessed_at)
) PARTITION BY RANGE (TO_DAYS(accessed_at)) (...);

设计要点

  • long_hash 索引用于长链去重:新增长链时先查 long_hash,命中则直接返回已有短链
  • access_log 按天分区,定期归档/删除,避免单表过大
  • BIGINT 而非 INT:INT 上限约 21 亿,日增 1 亿条不到一年就会爆

布隆过滤器的使用场景

  • 需求:对同一长链,每次都返回相同短链
  • 做法:每次生成短链后,在布隆过滤器写入 long_hash
  • 缺点:布隆过滤器判重可能有误判(False Positive),命中后仍需查 DB 确认

1.5 高并发优化

缓存策略

1
2
3
4
5
6
7
8
9
10
11
12
             ┌──────────┐
用户请求 ──→ │ CDN/LB │
└────┬─────┘

┌──────────┐
│ Redis 缓存│ ← 热点短链(TTL 7 天)
└────┬─────┘
缓存未命中

┌──────────┐
│ MySQL │ ← 全量数据
└──────────┘
  • Redis Key:shortlink:{short_code} → Value:长 URL JSON
  • 缓存预热:定时任务把即将过期的热门短链重新加载
  • 缓存穿透:布隆过滤器过滤不存在的短链,避免直接打 DB

预生成策略

  • 写入高峰前,离线批量预生成一批短链放入”号池”
  • 线上直接从号池取号,写入延迟从 50ms 降至 5ms
  • 号池低于阈值时,异步补充

发号器选择

  • 单机:数据库自增 ID(性能上限约 3000 QPS,不够用)
  • 分布式:Snowflake(Twitter 开源,64 bit:1+41+10+12)
  • 号段模式:每次从 DB 取一批号缓存在内存中(美团 Leaf)
  • 推荐:号段模式,兼顾性能与可靠性

1.6 301 vs 302 重定向

301(永久重定向) 302(临时重定向)
浏览器行为 本地缓存,后续直接跳转 每次都请求短链服务
服务端压力 低(只有首次请求) 高(每次都要处理)
统计准确性 无法统计后续点击 每次点击都有记录
修改灵活性 无法修改目标 URL 可以随时修改

选择建议

  • 需要统计点击量 → 302
  • 不需要统计,追求极致性能 → 301
  • 大多数生产系统选择 302,因为数据价值大于性能损失

1.7 完整架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
                              ┌─────────────────────┐
用户 ──→─ DNS ──→─ CDN ──→── │ API Gateway │
│ ┌───────┐ ┌──────┐ │
│ │限流 │ │鉴权 │ │
│ └───────┘ └──────┘ │
└─────────┬───────────┘

┌───────────────┼───────────────┐
↓ ↓ ↓
┌────────────┐ ┌────────────┐ ┌────────────┐
│ 短链生成服务 │ │ 重定向服务 │ │ 统计服务 │
│ (Focus: 写) │ │ (Focus: 读) │ │ (异步消费) │
└──────┬─────┘ └──────┬─────┘ └──────┬─────┘
│ │ │
↓ ↓ ↓
┌────────────┐ ┌───────────┐ ┌────────────┐
│ Leaf 发号器│ │Redis Cluster│ │ Kafka │
└────────────┘ └─────┬─────┘ └──────┬─────┘
│ │
↓ ↓
┌──────────────────────────┐
│ MySQL (主从 + 分区) │
└──────────────────────────┘