System Design Interview: URL Shortener to Feed System
The repeatable method: clarify → capacity estimation → API/DB → scale up → failure modes — worked on the three classics.
The method (use it out loud)
- Clarify scope: which features, read/write ratio, is the write one-time or continuous
- Capacity estimate (back-of-envelope): QPS, storage 5 years, bandwidth, memory
- API + data model: endpoints, tables, index direction
- Single-machine design first, THEN scale: cache, sharding, async queue, CDN
- Failure modes: single points, thundering herd, data loss windows
System design interviews grade the PROCESS as much as the result. Say your estimates out loud, state assumptions, then adjust when the interviewer probes — going mute while 'thinking' is scored as no signal.
Worked example: URL shortener
Requirements: short codes, redirects (read-heavy 100:1), custom aliases, expiry. Assume 100M new URLs/month, 10B reads/month.
容量速算(信封背面)
写:100M / 月 ≈ 40 QPS(月按 30 天 ≈ 2.6M 秒→ ~40/s,峰值 ×3 = 120 QPS)
读:10B / 月 ≈ 4000 QPS,峰值 ×3 ≈ 12k QPS(单 MySQL 抗不住 → 上缓存)
存储:100M × 500B(短码+长URL+元数据) ≈ 50GB/月 → 5 年 ≈ 3TB(单库够)
编码:6 位 Base62(62^6 ≈ 568 亿)绰绰有余
写路径:发号器(Snowflake/Redis incr)→ Base62 编码 → 存 MySQL(short=PK)
读路径:Redis 缓存 short→long(命中率 >95%)→ miss 回库
过期:TTL 或 异步清理任务
| Scheme | Pro | Con |
|---|---|---|
| Hash(MD5) 取后 6 位 | 无状态、可离线算 | 可能冲突、不可预测 |
| 自增 ID + Base62 | 短、有序、无冲突 | 发号器单点 / 可被遍历 |
| Snowflake + Base62 | 分布式、可排序 | 码稍长、机器位规划 |
Worked example: flash sale (秒杀)
Core reality: a flash sale is 99% rejected traffic. Design to reject cheaply before it touches the database.
分层防御(自外向内)
1. 客户端:按钮置灰 + 验证码(挡机器人)
2. CDN/网关:活动开始前静态页 + 限流(令牌桶)
3. Redis:预扣库存 Lua(单线程天然原子)+ 重复购买拦截(setnx uid:goods)
4. MQ:异步下单(削峰,库存成功才落库)
5. DB:最终扣减 + 兜底对账
- Inventory must be pre-deducted atomically in Redis with Lua: DECR if and only if stock > 0
- Orders land in a queue, DB consumption pace stays constant — the whole point of 削峰
- Oversell safety = final reconciliation: queue messages, DB rows, actual goods shipped must match
The oversell incident is always 'we had a cache' but reconciliation was missing. Redis Lua pre-deduction is necessary but not sufficient — a queue/DB reconciliation job is the actual guarantee.
方法论(要边想边说)
- 先问清楚:哪些功能、读写比、写入是一次性还是持续
- 容量估算(信封背面):QPS、5 年存储、带宽、内存
- API 与数据模型:接口、表、索引方向
- 先单机设计,再逐步扩展:缓存、分库分表、异步队列、CDN
- 故障模式:单点、惊群、数据丢失窗口
系统设计面试考核过程和结果同等重要。估算要说出声音、假设要说出口,面试官追问时再调整——一言不发地「思考」在评分上等于没有信号。
实战:短链系统
需求:生成短码、跳转重定向(读写比约 100:1)、支持自定义短码与过期。假设每月新增 1 亿条、读取 100 亿次。
容量速算(信封背面)
写:100M / 月 ≈ 40 QPS(月按 30 天 ≈ 2.6M 秒→ ~40/s,峰值 ×3 = 120 QPS)
读:10B / 月 ≈ 4000 QPS,峰值 ×3 ≈ 12k QPS(单 MySQL 抗不住 → 上缓存)
存储:100M × 500B(短码+长URL+元数据) ≈ 50GB/月 → 5 年 ≈ 3TB(单库够)
编码:6 位 Base62(62^6 ≈ 568 亿)绰绰有余
写路径:发号器(Snowflake/Redis incr)→ Base62 编码 → 存 MySQL(short=PK)
读路径:Redis 缓存 short→long(命中率 >95%)→ miss 回库
过期:TTL 或 异步清理任务
| 方案 | 优点 | 缺点 |
|---|---|---|
| Hash(MD5) 取后 6 位 | 无状态、可离线算 | 可能冲突、不可预测 |
| 自增 ID + Base62 | 短、有序、无冲突 | 发号器单点 / 可被遍历 |
| Snowflake + Base62 | 分布式、可排序 | 码稍长、机器位规划 |
实战:秒杀系统
核心事实:秒杀 99% 的流量都是要被拒绝的。设计目标是在流量碰到数据库之前,用最便宜的方式挡掉。
分层防御(自外向内)
1. 客户端:按钮置灰 + 验证码(挡机器人)
2. CDN/网关:活动开始前静态页 + 限流(令牌桶)
3. Redis:预扣库存 Lua(单线程天然原子)+ 重复购买拦截(setnx uid:goods)
4. MQ:异步下单(削峰,库存成功才落库)
5. DB:最终扣减 + 兜底对账
- 库存必须在 Redis 里用 Lua 原子预扣:DECR 当且仅当库存 > 0
- 订单先进队列,数据库消费速率恒定——这就是「削峰」的全部意义
- 防超卖的兜底是对账:队列消息数 = 订单数 = 实际发货数
超卖事故永远是「我做了缓存「但没做对账」。Redis Lua 预扣是必要不充分条件——队列/数据库层面的对账任务才是真正的保险。