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^7 ≈ 3.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, 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 (主从 + 分区) │ └──────────────────────────┘
|