Chapter 10

真实案例设计

知识落地的最终检验。设计短链接、通知系统、Feed 流与分布式文件存储,
在真实约束中综合运用前九章的所有知识。

案例一:设计短链接服务(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流 · 文件存储 │ └─────────────────────────────────────────────────────────────┘
▶ 最终面试心法