设计 URL 短链接服务(TinyURL)
经典系统设计题,考察存储、编码、扩展性三个核心维度。
第一步:明确需求
功能需求:
- 输入长 URL,返回短链(如
tinyurl.com/abc123) - 访问短链,重定向到原始长 URL
- (可选)自定义短码、设置过期时间
非功能需求(面试重点):
- 高可用:服务不能挂
- 低延迟:重定向要快,< 10ms
- 规模:假设每天 1 亿次写入,100 亿次读取(读多写少,约 100:1)
估算:
写 QPS:100M / 86400 ≈ 1200/s 读 QPS:1200 × 100 = 120,000/s 5年存储:1200/s × 5年 × 3×10^7s ≈ 1800亿条 每条约 500B → 约 90TB
第二步:核心问题——如何生成短码?
短码通常 6~8 位,用 [a-zA-Z0-9] 共 62 个字符。6 位能表示 62⁶ ≈ 568 亿个 URL,足够用。
方案一:哈希截断(MD5/SHA256 取前 6 位)
long_url → MD5 → "a3f8b2c91d..." → 取前6位 → "a3f8b2"
问题:哈希冲突。不同长链可能产生相同短码,需要冲突检测(查库,碰撞就换取后 6 位),逻辑复杂且有性能损耗。
方案二:自增 ID + 62 进制编码 ⭐ 推荐
数据库维护一个全局自增 ID,把十进制 ID 转成 62 进制字符串作为短码。
cpp
string encode(long long id) {
const string chars = "0123456789abcdefghijklmnopqrstuvwxyz"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ";
string res;
while (id > 0) {
res += chars[id % 62];
id /= 62;
}
reverse(res.begin(), res.end());
return res; // ID=1 → "1", ID=125 → "1Z", ...
}
long long decode(const string& code) {
const string chars = "0123456789abc...";
long long id = 0;
for (char c : code) {
id = id * 62 + chars.find(c);
}
return id;
}
优点: 天然无冲突,短码有序,实现简单。
问题: 单点瓶颈——全局 ID 生成器是单点,需要进一步设计。
第三步:系统架构
用户
│
├─ POST /shorten ──→ [写服务] ──→ [ID生成器] ──→ [MySQL/主库]
│ ──→ [Cache写入]
│
└─ GET /{code} ──→ [读服务] ──→ [Redis Cache]
│ Cache Miss
└──→ [MySQL/从库] ──→ 回写Cache
数据表设计(极简):
sql
CREATE TABLE url_mapping (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
code VARCHAR(8) UNIQUE,
long_url VARCHAR(2048) NOT NULL,
created_at DATETIME,
expires_at DATETIME
);
重定向方式:
301 Permanent Redirect:浏览器缓存,服务器压力小,但无法统计点击302 Temporary Redirect:每次都走服务器,可以统计点击数 ← 一般选这个
第四步:解决全局 ID 单点问题
方案一:号段模式(Segment)
ID 生成器一次批量申请一段 ID(如 10000 个),放到内存里慢慢用,用完再申请。数据库压力降低 10000 倍。
ID服务: 向DB申请 [1, 10001),缓存在内存
每次写请求直接从内存取,极快
用完后再申请 [10001, 20001)
方案二:多机 ID 生成(步长错开)
部署 3 台 ID 生成器,步长设为 3,起点分别为 1、2、3:
机器1: 1, 4, 7, 10, ... 机器2: 2, 5, 8, 11, ... 机器3: 3, 6, 9, 12, ...
无冲突,无需协调,天然分布式。
方案三:Snowflake
Twitter 的分布式 ID 方案,64 位 = 时间戳(41) + 机器ID(10) + 序列号(12),每毫秒可生成 4096 个不重复 ID,工业界主流选择。
第五步:缓存与扩展
缓存策略(读服务):
读写比 100:1,读服务加 Redis 缓存是最大的性能杠杆。
Key: short_code Value: long_url TTL: 与 URL 过期时间对齐(或固定 24h) 淘汰: LRU(热点 URL 永远在缓存里)
估算:20% 的热点 URL 贡献 80% 流量(二八定律),只缓存热点就够用。
水平扩展:
写服务: 无状态,加机器即可 读服务: 无状态,加机器即可 数据库: MySQL 主从(主写从读)+ 按 code 哈希分库分表 缓存: Redis Cluster
面试时的表达框架
1. 需求确认(2min) → 功能 + 规模估算 2. 核心设计(5min) → 短码生成方案选型,讲清楚62进制编码 3. 架构图(5min) → 读写分离、缓存层、ID生成器 4. 深挖细节(8min) → 单点问题怎么解、缓存怎么设计、301 vs 302 5. 扩展话题(5min) → 自定义短码、过期清理、点击统计、防滥用
面试官最想听的是你在每个决策点为什么选这个而不是那个,权衡过程比答案本身更重要。
