案例一:设计短链接服务(TinyURL)
需求分析
功能性需求
- 输入长 URL,生成唯一短链(7位字符)
- 访问短链,302 重定向到原始 URL
- 短链可设置过期时间
- 可选:点击量统计
非功能性需求
- 每天写:1亿次生成
- 每天读:1000亿次重定向
- 读写比:1000:1(读重型)
- 重定向延迟:< 50ms
- 可用性:99.99%
容量估算
写 QPS:100M / 86400 ≈ 1,200 QPS(峰值约 3K QPS)
读 QPS:100B / 86400 ≈ 1.2M QPS(峰值约 3M QPS)
存储(5年):
每条记录:short_url(7B) + long_url(100B) + 元数据(50B) ≈ 200B
每年记录数:100M × 365 = 365亿条
5年总量:365亿 × 5 × 200B ≈ 36 TB
缓存:
热门短链占 20% 数据,缓存命中这20%能满足 80% 读请求
缓存量 = 36TB × 20% × (1年份) ≈ 约 2TB(Redis 集群)
高层架构
TinyURL 系统架构:
Write Path(生成短链):
Client ──▶ API Gateway ──▶ URL Shortener Service
│
┌────────────────┼────────────────┐
▼ ▼ ▼
ID Generator Write to DB Return
(Snowflake) (short→long) short URL
Read Path(重定向):
Client ──▶ CDN(命中缓存直接返回)
│ Miss
▼
API Gateway ──▶ Redirect Service
│
┌──────────┴──────────┐
▼ ▼
Redis Cache DB Read Replica
(热门短链) (冷数据兜底)
│
▼
302 Redirect to Long URL
短链生成算法
方案A:随机哈希
short = base62(md5(long_url + salt))[0:7]
7位 Base62 = 62^7 = 3.5万亿种组合
问题:哈希碰撞检测(查DB)+ 同一 URL 每次不同结果
──────────────────────────────────────────────────────
方案B:分布式自增 ID(推荐)
① 全局 ID 生成器(Snowflake 算法)生成唯一 64 位 ID
② 将 ID 转换为 Base62 字符串(7位足够)
③ 不需要碰撞检测,天然唯一
Snowflake ID 结构(64 bit):
1bit符号位 | 41bit时间戳 | 10bit机器ID | 12bit序列号
└── 0 └── 毫秒级 └── 1024台机器 └── 每毫秒4096个
每毫秒可生成:1024 × 4096 = 4,194,304 个ID
Base62 字符集:0-9 a-z A-Z
62^7 = 3,521,614,606,208(35万亿),完全够用
方案C:发号器服务(简化)
DB 自增 ID(单点,性能约50K/s)
或:每台服务器预先申请一批ID(批量分配)
服务器A:1 ~ 10000,服务器B:10001 ~ 20000...
数据库设计
URL 映射表(存储在 MySQL 或 NoSQL):
url_mappings:
┌──────────┬──────────────────────────────────────────────┐
│ short_id │ VARCHAR(7) PRIMARY KEY │
│ long_url │ VARCHAR(2048) NOT NULL │
│ user_id │ BIGINT(哪个用户创建的) │
│ created │ TIMESTAMP │
│ expires │ TIMESTAMP(NULL = 永不过期) │
│ clicks │ BIGINT DEFAULT 0 │
└──────────┴──────────────────────────────────────────────┘
读写比 1000:1 → 读写分离是必须的
NoSQL 选型:Cassandra 的 KV 模型天然适合(short_id → long_url)
关系型:MySQL 配合读副本也可以撑住
案例二:设计通知系统(推送/邮件/SMS)
系统架构
通知系统全景架构:
触发方(各业务服务)
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 订单服务 │ │ 营销服务 │ │ 风控服务 │
└─────┬────┘ └─────┬────┘ └─────┬────┘
└─────────────┼──────────────┘
│ 发布通知事件
▼
┌─────────────────────────────────────────────────────┐
│ 通知服务(Notification Service) │
│ │
│ 接收通知请求 → 用户偏好查询 → 限流去重 → 路由分发 │
└──────────────────────────┬──────────────────────────┘
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Push队列 │ │ Email队列 │ │ SMS队列 │
│ (Kafka) │ │ (Kafka) │ │ (Kafka) │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│APNs/FCM │ │ SendGrid │ │ Twilio │
│(Apple/Google│ │ SES │ │ 阿里云短信 │
└─────────────┘ └─────────────┘ └─────────────┘
│
┌────▼────┐
│ 用户设备 │
└─────────┘
关键设计点
1. 用户偏好管理(User Preference)
┌────────────────────────────────────────────┐
│ 用户可以配置: │
│ - 接收哪些类型的通知(系统 / 营销 / 提醒) │
│ - 通过哪些渠道(推送 / 邮件 / 短信) │
│ - 勿扰时间(22:00 ~ 8:00 不发通知) │
│ 存储:Redis Hash(快速查询) │
└────────────────────────────────────────────┘
2. 限流去重(Rate Limiting & Dedup)
┌────────────────────────────────────────────┐
│ 同一用户同类通知:60秒内最多1条 │
│ 营销通知:24小时内最多3条 │
│ 去重ID:基于业务 Key 的幂等检查 │
│ 如:order-12345-shipped 只发一次 │
└────────────────────────────────────────────┘
3. 优先级队列
┌────────────────────────────────────────────┐
│ P0(紧急):验证码、支付成功 │
│ → 直接发送,不进队列 │
│ P1(重要):订单状态更新 │
│ → 高优先级队列,<30s 内发送 │
│ P2(普通):营销推送 │
│ → 普通队列,批量发送 │
└────────────────────────────────────────────┘
案例三:设计 Feed 流(Twitter / 朋友圈)
两种 Feed 架构模式
方案A:拉模式(Fan-out on Read)
用户A关注了 B、C、D
用户A刷新 Feed 时:
① 查询所有关注者:[B, C, D]
② 查询每人最新帖子:B的帖子、C的帖子、D的帖子
③ 合并排序(时间倒序)
④ 返回前20条
优点:写入简单(只写自己的时间线)
缺点:读取时需要大量查询,明星用户(关注1000人)读取慢
──────────────────────────────────────────────────────
方案B:推模式(Fan-out on Write)
用户B发布了一条推文:
① 写入到自己的帖子表
② 查询B的所有粉丝列表
③ 将这条帖子 ID 写入每个粉丝的 Feed 缓存
用户A刷新 Feed 时:
① 直接读取自己的 Feed 缓存(已经排好序了)
② 极快!O(1) 读取
优点:读取极快,O(1)
缺点:大V(千万粉丝)发一条帖子 → 需要写入千万个 Feed 缓存
写入放大严重!
──────────────────────────────────────────────────────
Twitter 的混合方案(推荐):
普通用户(粉丝 < 10万):推模式,实时扇出
大V(粉丝 > 10万):拉模式
用户刷新时:
① 读取自己的推送 Feed 缓存(普通好友的内容)
② 实时拉取关注的大V的最新帖子
③ 合并排序返回
Feed 流存储架构:
Post Table(帖子主表,持久化):
┌──────────┬──────────┬────────────┬──────────────────┐
│ post_id │ user_id │ content │ created_at │
└──────────┴──────────┴────────────┴──────────────────┘
Feed Cache(Redis,每个用户的时间线):
Key: feed:{user_id}
Type: Sorted Set
Score: 发布时间戳
Value: post_id
ZADD feed:user_123 1700000000 post_456
ZREVRANGE feed:user_123 0 19 ← 读取最新20条
EXPIRE feed:user_123 7days ← 缓存7天
存储量估算(1亿用户,每人Feed缓存500条):
每条:8字节(score) + 8字节(post_id) = 16字节
总量:1亿 × 500 × 16B = 800GB
需要约 1TB Redis 集群(考虑内存开销 × 1.2)
案例四:设计分布式文件存储(类 S3)
需求与规模
功能性需求
- 上传文件(支持大文件分片上传)
- 下载文件(支持断点续传)
- 删除文件
- 文件元数据查询
- 访问权限控制
非功能性需求
- 存储 1亿个文件,平均 1MB
- 总存储量:100 TB
- 耐久性:99.999999999%(11个9)
- 可用性:99.99%
- 读写比:1:1(写多读也多)
系统架构
分布式文件存储架构:
Client
│
├──上传──▶ API Server ──▶ Metadata Service ──▶ [Metadata DB]
│ │ (PostgreSQL)
│ │ 上传文件数据
│ ▼
│ Data Service(数据节点集群)
│ ┌──────────────────────────────────┐
│ │ Data Node 1 Data Node 2 ... │
│ │ 存储实际文件分片(Chunks) │
│ └──────────────────────────────────┘
│
└──下载──▶ API Server ──▶ Metadata Service(获取文件位置)
│
▼
Data Node(直接读取文件块)
│
▼
Client(流式返回)
元数据分离的好处:
- 元数据(小,结构化)用关系型DB,支持复杂查询
- 文件数据(大,非结构化)用对象存储优化,追加写入
大文件分片上传
分片上传(Multipart Upload)流程:
文件:large_video.mp4(2GB)
Step 1:客户端将文件切成 5MB 的块
Chunk 1:0 ~ 5MB
Chunk 2:5 ~ 10MB
...
Chunk 400:1995 ~ 2000MB
Step 2:并发上传所有 Chunk(并发数:5-10)
Client ──▶ Server:POST /upload?uploadId=abc&part=1 (Chunk 1)
Client ──▶ Server:POST /upload?uploadId=abc&part=2 (Chunk 2)
...(并发上传,失败的 Chunk 单独重试)
Step 3:通知服务端合并
Client ──▶ Server:POST /upload/complete?uploadId=abc
Server:验证所有 Chunk 完整 → 合并 → 返回最终 URL
Step 4:服务端存储策略
每个 Chunk 存到不同的 Data Node
同一 Chunk 在 3 个不同节点有副本(Replication Factor = 3)
任意 2 个节点宕机,文件仍然完整
数据耐久性:纠删码(Erasure Coding)
三副本 vs 纠删码对比:
三副本(Amazon S3 早期方案):
文件 → 复制 3 份 → 存到 3 个节点
存储开销:3x
可以损失:1 个节点
RS(6,3) 纠删码(Amazon S3 现代方案):
文件 → 切成 6 个数据块 + 生成 3 个校验块
存到 9 个节点,任意 3 个节点损坏可恢复
存储开销:9/6 = 1.5x(节省 50% 存储成本!)
代价:计算开销更大,恢复时需要读多个节点
┌───────┬─────────────┬────────────┬─────────────┐
│ │ 三副本 │ RS(6,3) │ RS(12,4) │
├───────┼─────────────┼────────────┼─────────────┤
│存储开销│ 3x │ 1.5x │ 1.33x │
│容灾能力│ 损失1个节点│ 损失3个节点│ 损失4个节点 │
│计算开销│ 低 │ 中 │ 高 │
└───────┴─────────────┴────────────┴─────────────┘
Facebook 仓储系统(f4):
热数据:三副本(访问频繁,延迟优先)
冷数据:RS(10,4)(访问少,存储成本优先)
ℹ 真实公司参考
Amazon S3:11个9的耐久性,底层使用纠删码 + 多地域复制。Google GFS/Colossus:将文件切成 64MB 的 Chunk,每个 Chunk 在不同机架存 3 个副本。Facebook Haystack:专为图片存储优化,将小文件合并到大文件(减少 inode 开销),读性能提升 3 倍。
系统设计知识体系回顾
10章知识图谱:
┌─────────────────────────────────────────────────────────────┐
│ 系统设计全貌 │
│ │
│ 方法论 │
│ └─ Ch1:RESHADED框架 · 需求分析 · 容量估算 · 架构图 │
│ │
│ 扩展性 │
│ ├─ Ch2:垂直扩展 vs 水平扩展 · 无状态 · Session管理 │
│ └─ Ch3:负载均衡 L4/L7 · 健康检查 · Anycast │
│ │
│ 存储与缓存 │
│ ├─ Ch4:缓存分层 · 4种策略 · 击穿/穿透/雪崩 │
│ └─ Ch5:关系型vs NoSQL · 分片 · 读写分离 · 分布式DB │
│ │
│ 异步与一致性 │
│ ├─ Ch6:消息队列 · Kafka · 幂等性 · Saga事务 │
│ └─ Ch7:CAP定理 · BASE · Paxos/Raft · 分布式锁 │
│ │
│ 服务架构与稳定性 │
│ ├─ Ch8:单体vs微服务 · 服务发现 · API网关 · 熔断器 │
│ └─ Ch9:限流算法 · 熔断 · 降级 · 超时 · SLA │
│ │
│ 综合案例 │
│ └─ Ch10:TinyURL · 通知系统 · Feed流 · 文件存储 │
└─────────────────────────────────────────────────────────────┘
▶ 最终面试心法
- 先问,再画:澄清需求比直接画图重要,展示你的沟通能力。
- 从简单开始:先给出能工作的方案,再谈如何优化和扩展。
- 主动说权衡:「这里我选择X而不是Y,因为...但X的缺点是...」
- 数量级要正确:估算 QPS、存储量时,数量级对即可,不要追求精确。
- 引用真实案例:「Twitter 在这里用了推拉混合模式,因为...」
- 画图贯穿始终:边讲边画,让面试官能一直跟上你的思路。