Module 11: 综合实战与架构演进
📖 深度参考手册 — 本模块属于理论参考,非主线必读。 主线学习路径见 README.md。 当你在项目实战中遇到相关问题时,回来查阅。
前8个模块建立了系统设计的知识体系,这个模块把所有知识串联起来——从架构演进、技术选型到运维保障,形成完整的系统设计思维。
11.1 从单体到微服务
Section titled “11.1 从单体到微服务”定义:软件架构的演进通常遵循这个路径:单体(Monolith) → 模块化单体(Modular Monolith) → 微服务(Microservices)。单体是所有功能打包在一个进程中运行;模块化单体是在单体内部划分清晰的模块边界,但仍然是一个部署单元;微服务是将每个模块独立部署为独立的服务,通过网络通信。关键原则:不要从微服务开始——几乎所有成功的微服务系统都是从单体演化而来的。
为什么重要:微服务带来的好处(独立部署、独立扩容、技术栈自由)只有在团队和系统达到一定规模后才显现。过早拆分微服务会带来巨大的运维开销(服务发现、分布式事务、网络延迟、调试复杂度),在早期阶段这些开销远远超过收益。Martin Fowler称之为”Monolith First”——先用单体验证业务模型,等到单体的痛点(部署冲突、模块耦合、扩展瓶颈)真正出现时再拆分。
案例:所有11个系统的”第一版”都可以(也应该)是单体。
以 URL Shortener 为例,架构演进过程:
V1 单体(0-10万用户):
┌─────────────────────────────────┐│ 单体应用 ││ ┌───────────┐ ┌──────────────┐ ││ │ URL缩短 │ │ URL重定向 │ ││ │ 模块 │ │ 模块 │ ││ ├───────────┤ ├──────────────┤ ││ │ 统计分析 │ │ 用户管理 │ ││ │ 模块 │ │ 模块 │ ││ └───────────┘ └──────────────┘ ││ ↕ ││ ┌──────────────────┐ ││ │ PostgreSQL │ ││ └──────────────────┘ │└─────────────────────────────────┘
优点: 开发快、部署简单、调试方便、一个人能搞定够用: 10万用户、每天几十万次重定向V2 模块化单体(10-100万用户):
┌─────────────────────────────────────┐│ 模块化单体(清晰边界) ││ ┌───────────┐ ┌──────────────┐ ││ │ URL模块 │ │ 统计模块 │ ││ │ interface │───→│ interface │ ││ │ 定义清晰 │ │ 独立数据表 │ ││ └───────────┘ └──────────────┘ ││ 模块间通过定义好的接口通信 ││ 每个模块有独立的数据表(不直接访问 ││ 其他模块的表) │└─────────────────────────────────────┘
改变: 划清模块边界、定义模块接口、拆分数据表所有权目的: 为将来可能的微服务拆分做准备,但现在还不拆V3 微服务(100万+用户,团队>20人):
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐│ URL服务 │ │ 重定向 │ │ 统计服务 │ │ 用户服务 ││ │ │ 服务 │ │ │ │ ││ MySQL │ │ Redis │ │ ClickHouse│ │ PostgreSQL│└──────────┘ └──────────┘ └──────────┘ └──────────┘
拆分的信号(什么时候该拆了):- 重定向QPS是URL创建的1000倍 → 需要独立扩容- 统计分析需要列式存储(ClickHouse) → 技术栈不同- 20+开发者同时修改一个代码库 → 部署冲突频繁先想一想 🤔 Chat System 的哪些模块最可能最先被拆分为独立微服务?为什么?
点击查看解析
最先拆分的候选模块:
- 消息存储/推送服务:
- 原因:消息的读写QPS远高于其他模块(用户管理、群组管理等),需要独立扩容。
- 技术:可能需要专门的存储引擎(如Cassandra用于消息存储),与其他模块的PostgreSQL不同。
- WebSocket网关/连接服务:
- 原因:维护长连接是资源密集型操作(每个连接占用内存和文件描述符),需要独立的扩容策略。
- 特点:连接服务是有状态的(每个连接绑定到特定服务器),而其他服务可以是无状态的。
- 文件/媒体服务:
- 原因:图片和视频的存储、压缩、缩略图生成是CPU和IO密集型,与文本消息的处理模式完全不同。
- 技术:需要对象存储(S3)、CDN、图片处理库,独立部署更合理。
不急于拆分的模块:
- 用户管理/认证:QPS低、逻辑简单,拆分收益小。
- 群组管理:和消息服务耦合度高,过早拆分会引入大量跨服务调用。
核心原则:拆分的依据是”痛点”,不是”理论上应该拆”。只有当某个模块的扩展需求、技术需求或团队需求与其他模块明显不同时,才值得拆分。
11.2 数据库选型决策树
Section titled “11.2 数据库选型决策树”定义:不同类型的数据库针对不同的数据模型和访问模式做了优化——关系型(RDBMS) 适合结构化数据和事务;文档型(Document) 适合半结构化数据和灵活schema;列式(Columnar) 适合分析型查询;图(Graph) 适合关系密集的数据;时序(Time-Series) 适合带时间戳的度量数据;搜索引擎(Search) 适合全文检索。数据库选型的核心是匹配你的主要访问模式,而不是追逐技术热点。
为什么重要:选错数据库是最昂贵的技术债务之一——迁移数据库通常涉及数据迁移、代码重写和数据一致性验证,成本远高于更换应用框架。选择数据库时要思考的核心问题:数据结构是什么?主要查询模式是什么?对一致性和可用性的要求各是什么?数据量和增长速率是多少?
决策树:
你的数据需要ACID事务吗?├── 是 → 数据结构是固定schema吗?│ ├── 是 → 关系型 (PostgreSQL / MySQL)│ └── 否 → 文档型+事务 (MongoDB 4.0+)│└── 否 → 你的主要查询模式是? ├── 按key查询 → KV存储 (Redis / DynamoDB) ├── 全文搜索 → 搜索引擎 (Elasticsearch) ├── 图遍历 → 图数据库 (Neo4j / Neptune) ├── 时间范围聚合 → 时序数据库 (TimescaleDB / InfluxDB) ├── 分析/报表 → 列式数据库 (ClickHouse / BigQuery) └── 高吞吐写入 → 宽列存储 (Cassandra / HBase)案例:11个系统的数据库选型对比表
| 系统 | 主数据库 | 原因 | 辅助存储 |
|---|---|---|---|
| URL Shortener | Redis/DynamoDB | 简单KV查询(短码→长URL),极高读QPS | PostgreSQL(用户/统计) |
| Web Crawler | MongoDB | 半结构化页面数据,schema灵活 | Elasticsearch(搜索索引) |
| News Feed | Redis(Feed列表) | 按用户ID读取预计算的帖子列表 | PostgreSQL(帖子/用户) |
| Chat System | Cassandra | 消息按(会话ID,时间)范围查询,写入吞吐极高 | Redis(在线状态/未读数) |
| Search Engine | Elasticsearch | 核心需求就是全文检索 | 数据库存原始文档 |
| YouTube | Bigtable/Cassandra | 视频元数据写入量大 | Redis(热门缓存)、ClickHouse(统计) |
| Google Drive | PostgreSQL | 文件元数据需要事务(权限、共享) | S3(文件内容)、Elasticsearch(搜索) |
| Proximity Service | PostGIS/Redis | 地理空间查询(附近的店铺) | Elasticsearch(地理搜索) |
| Google Maps | 图数据库 | 路径规划=图遍历 | PostgreSQL(POI)、Redis(瓦片缓存) |
| Hotel Reservation | PostgreSQL | 预订需要强ACID事务(防超卖) | Redis(价格缓存)、Elasticsearch(搜索) |
| Gaming Leaderboard | Redis(Sorted Set) | 天然支持排行榜操作(ZADD/ZRANK) | PostgreSQL(玩家信息) |
先想一想 🤔 Hotel Reservation 为什么必须用关系型数据库?用Redis能实现”不超卖”吗?
点击查看解析
Hotel Reservation必须用关系型数据库的核心原因:ACID事务
预订一个房间涉及多步操作,必须原子性完成:
BEGIN TRANSACTION;-- 1. 检查房间是否可用SELECT * FROM rooms WHERE id = 101 AND status = 'available' FOR UPDATE;-- 2. 扣减库存UPDATE rooms SET status = 'booked', guest_id = 'alice' WHERE id = 101;-- 3. 创建订单INSERT INTO reservations (room_id, guest_id, checkin, checkout) VALUES (...);-- 4. 扣款INSERT INTO payments (reservation_id, amount) VALUES (...);COMMIT;如果第3步成功但第4步失败,事务回滚——房间状态恢复为available。关系型数据库的ACID事务天然保证了这一点。
Redis能防超卖吗?可以,但更复杂:
Redis可以用Lua脚本实现原子操作:
-- Redis Lua脚本local available = redis.call('GET', 'room:101:status')if available == 'available' thenredis.call('SET', 'room:101:status', 'booked')redis.call('SET', 'room:101:guest', 'alice')return 1 -- 预订成功elsereturn 0 -- 已被预订end但问题是:
- 没有跨key事务:如果还要同时创建订单记录和扣款记录,Redis无法保证原子性
- 持久化不可靠:Redis的RDB/AOF持久化在极端情况下可能丢数据(如宕机)
- 复杂查询困难:“查询3月20日到3月25日所有可用房间”在Redis中需要手动维护索引
最佳实践:PostgreSQL做主数据库保证正确性,Redis做缓存加速查询(如缓存热门日期的房间可用性)。
11.3 多层缓存设计
Section titled “11.3 多层缓存设计”定义:多层缓存是在数据从源头(数据库)到用户之间的每一层都设置缓存的策略。从近到远依次为:客户端缓存(浏览器/APP本地缓存)→ CDN缓存(全球边缘节点)→ 反向代理缓存(Nginx/Varnish)→ 应用层缓存(Redis/Memcached)→ 数据库缓存(查询缓存/Buffer Pool)。每一层缓存都能拦截一部分请求,避免打到下一层,逐层减少对数据源的压力。
为什么重要:缓存是性能优化最有效的手段。一次数据库查询可能需要10ms,一次Redis读取只需0.5ms,一次本地缓存命中几乎是0ms。在高并发系统中,如果每次请求都打到数据库,数据库很快就会成为瓶颈。多层缓存的目标是:让尽可能多的请求在尽可能靠近用户的地方被满足。
案例:Google Maps 瓦片的多层缓存 是最经典的多层缓存案例。
Google Maps将地图切分为瓦片(Tile)——固定大小(如256x256像素)的地图图片,按层级(zoom level)、行、列编号。用户在地图上拖动和缩放时,客户端只加载当前视口需要的瓦片。
用户在北京打开Google Maps,查看故宫附近的地图
请求: GET /tile/z=15/x=27248/y=12660.png
缓存层级和命中率:
第1层: 客户端缓存(浏览器/APP) ┌──────────────────────────────────────┐ │ 用户5分钟前刚看过这个区域 │ │ 浏览器本地有缓存 → 命中!0ms │ │ 命中率: ~30%(用户经常在同一区域活动) │ └──────────────────────────────────────┘ ↓ 未命中
第2层: CDN(北京边缘节点) ┌──────────────────────────────────────┐ │ 北京有很多用户看过故宫附近的地图 │ │ CDN边缘节点有缓存 → 命中!~5ms │ │ 命中率: ~60%(热门区域被大量缓存) │ └──────────────────────────────────────┘ ↓ 未命中
第3层: 反向代理缓存(区域数据中心Nginx) ┌──────────────────────────────────────┐ │ 区域数据中心的Nginx也缓存了热门瓦片 │ │ 命中率: ~5%(CDN已经挡住了大部分) │ │ 延迟: ~20ms │ └──────────────────────────────────────┘ ↓ 未命中
第4层: 应用层缓存(Redis集群) ┌──────────────────────────────────────┐ │ Redis中缓存了最近被请求的瓦片 │ │ 命中率: ~3% │ │ 延迟: ~2ms │ └──────────────────────────────────────┘ ↓ 未命中
第5层: 瓦片存储服务(从源头生成/读取) ┌──────────────────────────────────────┐ │ 从预渲染的瓦片存储中读取 │ │ 或者实时渲染(极少见,通常是预渲染好的) │ │ 延迟: ~50-200ms │ │ 只有~2%的请求到达这里 │ └──────────────────────────────────────┘效果:
100万次瓦片请求: 第1层命中: 30万次 (客户端,0ms) 第2层命中: 42万次 (CDN,5ms) 第3层命中: 3.5万次 (反向代理,20ms) 第4层命中: 2.1万次 (Redis,2ms) 第5层: 2.4万次 (源头,100ms)
→ 只有2.4%的请求打到源头!→ 平均延迟 ≈ 0.3×0 + 0.42×5 + 0.035×20 + 0.021×2 + 0.024×100 ≈ 5.2ms缓存失效策略——地图瓦片的独特优势:
- 地图数据极少变化(道路不会每天改)→ 缓存TTL可以设得很长(如30天)
- 瓦片的key是确定的(z/x/y)→ 缓存key非常规整,不存在穿透问题
- 当地图数据更新时(如新建了一条路),只需要失效受影响区域的瓦片 → 精确定向失效
先想一想 🤔 如果是 Hotel Reservation 的房间价格缓存,能像地图瓦片一样设置30天的缓存TTL吗?如何设计酒店价格的缓存策略?
点击查看解析
绝对不能设30天TTL! 酒店价格是动态的——随供需、季节、竞争对手价格实时变化。缓存过期的价格可能导致:
- 用户看到低价,点进去发现涨价了 → 体验差
- 用户看到高价,不点了,但实际已经降价 → 丢失订单
酒店价格缓存策略:
短TTL + 主动失效:
缓存: RedisTTL: 5分钟(价格5分钟内可能不变)主动失效: 当价格更新时,立即删除相关缓存→ 价格变化事件(Kafka) → 缓存失效消费者 → DEL cache:hotel:101:price读取时校验:
用户搜索页面: 从缓存读取价格(快,可能略过时)用户点击预订: 从数据库读取最新价格(准确)如果搜索页价格和预订页价格不一致 → 提示用户"价格已更新"分层缓存 + 不同TTL:
客户端缓存: TTL=1分钟(搜索结果页面短暂缓存)CDN: 不缓存(价格是动态的、个性化的)Redis: TTL=5分钟(热门酒店价格)预计算 + 缓存:
每小时跑一次定时任务:对热门酒店×热门日期的组合预计算价格写入Redis,TTL=1小时这样用户搜索时直接从缓存读取,不需要实时计算核心区别:地图瓦片是静态内容→长缓存;酒店价格是动态内容→短缓存+主动失效。缓存策略必须匹配数据的变化频率。
11.4 故障设计 (Design for Failure)
Section titled “11.4 故障设计 (Design for Failure)”定义:故障设计是指在系统设计阶段就假设所有组件都会故障,并预先设计好故障处理机制。核心四板斧:重试(Retry) — 对临时故障自动重试;熔断(Circuit Breaker) — 当下游服务持续故障时切断调用,防止级联故障;降级(Degradation) — 当核心功能不可用时提供次优但可用的替代方案;限流(Rate Limiting) — 控制请求速率,防止过载。
为什么重要:在分布式系统中,故障不是”如果发生”而是”何时发生”的问题。网络会抖动、服务会崩溃、磁盘会写满、数据库会过载。如果系统没有故障处理机制,一个组件的故障会像多米诺骨牌一样导致整个系统崩溃(级联故障)。好的故障设计让系统在部分故障时仍能提供降级但可用的服务(优雅降级)。
案例:以所有系统的依赖故障处理来详细说明四板斧。
1. 重试 (Retry)
URL Shortener 创建短链接时写入数据库超时:
策略: 指数退避 + 抖动 (Exponential Backoff with Jitter)
第1次尝试: 请求 → 超时等待: 100ms + random(0, 50ms) ≈ 120ms第2次尝试: 请求 → 超时等待: 200ms + random(0, 100ms) ≈ 250ms第3次尝试: 请求 → 成功!
为什么要抖动?如果1000个客户端同时超时,都在100ms后重试→ 1000个请求同时打到已经过载的数据库 → 更过载 → "重试风暴"加了随机抖动后,重试请求分散在不同时间点,减少冲击重试的前提:操作必须是幂等的!(见7.8)
2. 熔断 (Circuit Breaker)
Search Engine 调用排序服务(Ranking Service):
正常状态 (CLOSED): 所有请求正常转发 → 排序服务开始报错(5分钟内错误率 > 50%) → 触发熔断!
熔断状态 (OPEN): 所有请求立即返回错误,不调用排序服务 → 等待30秒冷却期
半开状态 (HALF-OPEN): 放行少量请求试探 → 如果试探成功 → 恢复正常 (CLOSED) → 如果试探失败 → 继续熔断 (OPEN)
成功率恢复 CLOSED ←──────── HALF-OPEN │ ↑ │ 错误率>阈值 │ 冷却期到 ↓ │ OPEN ────────────────┘如果没有熔断:排序服务已经挂了,但Search Engine还在拼命调用 → 线程全部阻塞在等待排序服务响应上 → Search Engine本身也无法处理新请求 → 级联故障。
3. 降级 (Degradation)
News Feed 推荐服务不可用:
正常模式: 用户刷Feed → 调用推荐服务 → 返回个性化排序的帖子列表
降级模式(推荐服务熔断后): 用户刷Feed → 推荐服务不可用 → 降级: 按时间倒序返回关注者的最新帖子(无个性化排序) → 用户体验差一点,但至少能用
更多降级案例: YouTube 推荐服务挂了 → 显示热门视频而非个性化推荐 Google Maps 路况服务挂了 → 显示无路况的基础地图 Hotel Reservation 价格服务挂了 → 显示"价格加载中,请稍后"而非白屏4. 限流 (Rate Limiting)
URL Shortener 防止滥用:
限流规则: - 匿名用户: 10次/分钟 - 注册用户: 100次/分钟 - API Key用户: 1000次/分钟
实现方式 (令牌桶算法): 每分钟向桶中放入100个令牌 每次请求消耗1个令牌 桶空了 → 返回 429 Too Many Requests
时间线: [===令牌桶===] 100个令牌 请求1: 消耗1个 → 99个 → 200 OK 请求2: 消耗1个 → 98个 → 200 OK ... 请求100: 消耗1个 → 0个 → 200 OK 请求101: 桶空了 → 429 Too Many Requests ...1分钟后桶重新填满... 请求102: 消耗1个 → 99个 → 200 OK先想一想 🤔 Chat System的消息发送功能,应该如何组合使用重试、熔断、降级、限流四种机制?
点击查看解析
限流(第一道防线):
- 每个用户每分钟最多发送60条消息(防刷屏)
- 每个群聊每分钟最多200条消息(防群消息爆炸)
重试(对消息存储层):
- 消息写入Cassandra失败 → 重试3次,指数退避
- 前提:消息ID是幂等的(client_message_id去重)
熔断(对附属服务):
- 消息推送通知服务(FCM/APNs)挂了 → 熔断
- 消息搜索索引服务挂了 → 熔断
- 核心消息存储不能熔断(否则就完全不能发消息了)
降级(分级降级策略):
Level 0 (正常): 全功能Level 1: 推送通知服务挂了→ 降级: 消息仍然存储和推送(WebSocket),但不发手机通知→ 用户打开APP能看到消息,但不在状态栏看到通知Level 2: 搜索索引服务挂了→ 降级: 消息搜索功能暂时不可用,但收发消息正常Level 3: 在线状态服务挂了→ 降级: 不显示"在线/离线"状态,但消息收发正常Level 4: 消息存储写入失败→ 降级: 消息暂存到本地(客户端)队列,待服务恢复后补发→ 提示用户"消息发送中,网络恢复后将自动发送"核心原则:把功能按重要性分级,最核心的功能(消息收发)有最强的保护,辅助功能(通知、搜索、在线状态)可以独立降级而不影响核心功能。
11.5 可观测性
Section titled “11.5 可观测性”定义:可观测性(Observability)是指通过系统的外部输出来推断系统内部状态的能力。三大支柱:日志(Logs) — 离散的事件记录,告诉你”发生了什么”;指标(Metrics) — 随时间变化的数值度量,告诉你”整体状态如何”;链路追踪(Traces) — 记录一个请求经过所有服务的完整路径,告诉你”一个请求经历了什么”。三者互补,缺一不可。
为什么重要:在分布式系统中,问题往往隐藏在多个服务的交互中——单看任何一个服务的日志都不能发现问题。可观测性是你在生产环境中理解系统行为、定位故障、优化性能的唯一手段。一句话:你无法修复你看不见的问题。
案例:所有生产系统都需要可观测性。以一个综合场景说明三大支柱如何协作。
场景:用户报告”搜索很慢”
第1步:看指标(Metrics)— 快速判断问题范围
Grafana仪表盘: ┌─────────────────────────────────────────┐ │ Search API P99 延迟 │ │ ┌────────────────────────────┐ │ │ │ ╱╲ │ │ │ │ ╱ ╲ 正常: 200ms │ │ │ │────╱────╲──── 当前: 2000ms│ │ │ │ ╱ ╲ │ │ │ └────────────────────────────┘ │ │ 异常!P99延迟在14:00突然升高10倍 │ │ │ │ Search DB CPU使用率 │ │ ┌────────────────────────────┐ │ │ │ ╱╲ │ │ │ │─────────╱──────── 95%! │ │ │ └────────────────────────────┘ │ │ DB CPU在同一时间飙升 │ └─────────────────────────────────────────┘
→ 初步判断: 问题可能在数据库层第2步:看链路追踪(Traces)— 定位具体瓶颈
Jaeger链路追踪: 请求: GET /api/search?q=hotel+beijing
├── API Gateway 2ms ├── Query Service 5ms │ ├── 查询解析 1ms │ └── 调用Index Service ├── Index Service 1800ms ← 瓶颈! │ ├── 缓存查找(Redis) 1ms (MISS) │ └── 数据库查询 1795ms ← 这里! ├── Ranking Service 50ms └── Snippet Service 30ms
总计: 1887ms
→ 锁定: Index Service的数据库查询耗时1795ms(正常应该<50ms)第3步:看日志(Logs)— 找到根因
Index Service 日志 (ELK/Loki):
14:00:01 [WARN] Slow query: SELECT * FROM inverted_index WHERE term = 'hotel' AND region = 'beijing' Duration: 1823ms Rows examined: 12,000,000 Index used: NONE ← 根因!没有走索引!
14:00:01 [INFO] Index rebuild started at 13:55 Status: IN PROGRESS → 索引正在重建中,查询走了全表扫描!根因:13:55开始的索引重建操作导致数据库查询无法使用索引,走全表扫描,查询从50ms飙升到1800ms。
三大支柱的协作: 指标(Metrics) → 发现异常 → "延迟升高了" (定位到时间和范围) 追踪(Traces) → 定位瓶颈 → "是Index Service的DB查询慢" (定位到组件) 日志(Logs) → 找到根因 → "索引重建导致全表扫描" (定位到原因)
如果只有其中一个: - 只有指标: 知道慢了,但不知道哪里慢 - 只有追踪: 知道DB查询慢,但不知道为什么慢 - 只有日志: 能找到慢查询日志,但在海量日志中很难发现先想一想 🤔 如果系统有100个微服务,每秒百万级请求,日志量巨大。如何在不增加过多成本的情况下保持有效的可观测性?
点击查看解析
核心策略:采样 + 分级
- 链路追踪采样:不需要记录每个请求的完整链路。
- 正常请求:采样率1%(每100个请求只记录1个的完整链路)
- 慢请求(>P99延迟):100%记录
- 错误请求:100%记录
- 这样既能发现问题,又不会产生海量追踪数据
- 日志分级:
- ERROR/WARN:全量记录,保留90天
- INFO:采样记录(10%),保留30天
- DEBUG:默认关闭,只在排查问题时临时开启特定服务的DEBUG级别
- 结构化日志(JSON格式)比纯文本更容易查询和聚合
- 指标聚合:
- 不存储每个请求的指标,而是预聚合为统计量(P50/P95/P99/平均值/计数)
- 每15秒一个数据点(而不是每秒),保留1年
- 用Prometheus + Grafana,成本可控
- 关联:
- 每个请求生成唯一的 trace_id
- 日志、指标、追踪都带上 trace_id
- 在Grafana中点击异常指标 → 跳转到对应时间段的追踪 → 跳转到具体日志
- 这就是”可观测性的关联”(Correlation),是三大支柱发挥最大价值的关键
- 成本优化:
- 冷热分离:最近7天的数据在SSD(快速查询),7天前的数据在HDD/S3(低成本存储)
- 按需开启:正常时用低采样率,告警触发时自动提高采样率
11.6 知识点全景图
Section titled “11.6 知识点全景图”定义:系统设计不是一堆孤立知识点的集合,而是一个互相关联的知识网络。每个知识点在不同的系统中扮演不同的角色,理解它们之间的关系比记住每个知识点的定义更重要。这个知识点将所有模块的内容串联起来,形成一个”系统设计知识检查清单”。
为什么重要:面试和实际工作中的系统设计都需要全局思维——不是知道某个知识点就行,而是要知道在什么场景下用什么知识点、用了A是否需要配合B、以及不同选择之间的权衡。知识全景图帮你建立这种全局视角。
系统设计知识检查清单:
拿到一个系统设计问题时,按以下维度逐一检查:
1. 需求分析 □ 功能需求是什么?核心用例有哪些? □ 非功能需求:QPS、延迟、可用性、一致性要求 □ 数据量估算:存储量、增长速率
2. 数据层 □ 数据模型:实体和关系 □ 数据库选型:关系/文档/KV/列式/图/时序/搜索?(9.2) □ 数据分区:按什么key分片?哈希/范围/地理? □ 数据复制:主从/多主/无主?同步/异步? □ 一致性要求:强一致/最终一致/因果一致?
3. 缓存层 □ 需要缓存吗?缓存什么数据? □ 缓存策略:Cache-Aside / Write-Through / Write-Behind? □ 多层缓存设计 (9.3) □ 缓存失效:TTL / 主动失效 / 事件驱动失效? □ 缓存问题:穿透 / 击穿 / 雪崩的防护
4. 计算层 □ 同步还是异步处理? □ 批处理还是流处理?(8.1) □ 需要消息队列吗?用在哪里? □ 写入密集还是读取密集?CQRS适用吗?(8.8)
5. 网络层 □ API设计:REST / gRPC / WebSocket? □ 负载均衡:L4/L7、算法选择 □ 服务发现 (7.9) □ CDN是否适用?
6. 可靠性 □ 故障设计:重试/熔断/降级/限流 (9.4) □ 幂等性设计 (7.8) □ 脑裂防护 (7.7) □ 数据备份和恢复策略
7. 可扩展性 □ 哪些组件需要水平扩展?瓶颈在哪里? □ 有状态组件如何扩展? □ 架构演进路径 (9.1)
8. 可观测性 (9.5) □ 关键指标有哪些? □ 告警规则 □ 链路追踪如何实现?知识点 × 系统矩阵:
| 知识点 | URL短链 | 爬虫 | 信息流 | 聊天 | 搜索 | 视频 | 云盘 | 附近 | 地图 | 酒店 | 排行榜 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 网络不可靠 | ★ | ||||||||||
| 超时与故障检测 | ★ | ||||||||||
| 时钟问题 | ★ | ||||||||||
| 逻辑时钟 | ★ | ||||||||||
| 拜占庭容错 | ★ | ||||||||||
| Quorum读写 | |||||||||||
| 脑裂 | ★ | ||||||||||
| 幂等性 | ★ | ★ | |||||||||
| 服务发现 | ● | ● | ● | ● | ● | ● | ● | ● | ● | ● | ● |
| 批处理 | ★ | ★ | ★ | ||||||||
| 流处理 | ● | ★ | ★ | ||||||||
| MapReduce | ★ | ||||||||||
| DAG数据流 | ★ | ||||||||||
| 时间语义 | ★ | ||||||||||
| 窗口操作 | ★ | ||||||||||
| 流表对偶 | ★ | ||||||||||
| Event Sourcing | ★ | ||||||||||
| CQRS | ★ | ||||||||||
| Lambda/Kappa | ★ | ||||||||||
| 单体→微服务 | ★ | ||||||||||
| 数据库选型 | ★ | ★ | ★ | ★ | ★ | ★ | ★ | ★ | ★ | ★ | ★ |
| 多层缓存 | ★ | ||||||||||
| 故障设计 | ● | ● | ● | ● | ● | ● | ● | ● | ● | ● | ● |
| 可观测性 | ● | ● | ● | ● | ● | ● | ● | ● | ● | ● | ● |
★ = 该知识点在此系统中是核心/最佳案例 ● = 该知识点在此系统中有应用但不是重点案例
先想一想 🤔 看着上面的矩阵表,有几个知识点几乎在所有系统中都出现(如服务发现、故障设计、可观测性)。这些知识点和只在特定系统中出现的知识点(如拜占庭容错、Event Sourcing),在面试中应该如何分配学习时间?
点击查看解析
投入时间的优先级:
第一优先级(必须精通)— “通用基础”:
- 数据库选型、缓存设计、故障设计、可观测性
- 这些知识点在每个系统设计面试中都会用到
- 投入60%的学习时间
第二优先级(需要理解)— “常用模式”:
- 幂等性、批处理/流处理、CQRS、消息队列
- 在大多数系统中会涉及
- 投入25%的学习时间
第三优先级(了解即可)— “特定场景”:
- 拜占庭容错、逻辑时钟、Event Sourcing、Lambda/Kappa
- 只在特定类型的系统中才需要
- 投入15%的学习时间
- 但是:面试中提到这些知识点会是很好的加分项,展示你的知识深度
面试策略:
- 先用第一优先级的知识搭建系统骨架(数据库、缓存、容错、监控)—— 这是”及格线”
- 再根据具体系统的特点引入第二优先级的知识(如聊天系统引入消息队列、信息流引入CQRS)—— 这是”良好”
- 最后在关键环节深入讨论第三优先级的知识(如聊天系统讨论逻辑时钟解决消息排序)—— 这是”优秀”
学习建议:用矩阵表来指导刷题——不要只刷系统,要刷知识点。确保每个知识点至少在2-3个系统中练习过应用。
练习1:全新系统设计
Section titled “练习1:全新系统设计”选择一个未在11个案例中的系统(建议选一个你常用的产品),用以下框架完整分析:
- 需求分析:核心功能、QPS估算、数据量估算
- 数据层设计:数据模型、数据库选型(用9.2决策树)、分区策略
- 缓存设计:多层缓存架构(参考9.3)
- 计算层设计:同步/异步、批/流、是否需要CQRS
- 可靠性设计:故障设计四板斧的具体应用
- 可观测性:关键指标、告警规则
- 演进路径:V1单体→V2模块化→V3微服务的具体拆分计划
点击查看解析(以Spotify音乐流媒体为例)
1. 需求分析
核心功能:
- 音乐搜索、浏览、播放
- 个性化推荐(每日推荐、发现周刊)
- 播放列表创建/分享
- 离线下载
规模估算(假设1亿月活用户):
- 每天播放10亿首歌(人均10首)
- 每首歌平均4MB,但通过流式传输+CDN不需要全部存储在内存
- 歌曲库:约1亿首歌 × 10MB = 1PB 音频文件
- 元数据:1亿首歌 × 1KB = 100GB
QPS估算:
- 搜索QPS:~50,000/s(峰值)
- 播放请求QPS:~100,000/s(峰值)
- 推荐API QPS:~30,000/s
2. 数据层设计
| 数据 | 数据库 | 理由 |
|---|---|---|
| 用户/歌曲元数据 | PostgreSQL | 需要事务、关系查询 |
| 音频文件 | S3 + CDN | 海量静态文件存储 |
| 搜索索引 | Elasticsearch | 全文搜索+模糊匹配 |
| 用户行为日志 | Kafka → ClickHouse | 高吞吐写入+分析查询 |
| 推荐特征 | Redis | 低延迟读取 |
| 社交关系(关注) | PostgreSQL或Neo4j | 看规模和查询复杂度 |
分区策略:
- 用户数据:按user_id哈希分片
- 歌曲元数据:按song_id哈希分片
- 行为日志:按日期范围分区
3. 缓存设计
客户端缓存: 最近播放的歌曲列表、用户偏好设置 (SQLite)CDN: 音频文件(命中率>95%,热门歌曲被大量缓存)Redis L1: 热门歌曲元数据、推荐结果 (TTL=1小时)Redis L2: 用户播放列表、最近播放历史 (TTL=24小时)DB Buffer Pool: PostgreSQL自带的缓存4. 计算层设计
- 播放请求:同步处理(低延迟要求)
- 推荐计算:批处理(每日离线计算)+ 流处理(实时信号调整)= Lambda架构
- 搜索索引更新:近实时流处理(新歌曲上架后几秒可搜到)
- CQRS:是的。写入(记录播放行为)和读取(获取推荐/排行榜)用不同数据结构
5. 可靠性设计
重试: 音频流播放中断 → 客户端自动从断点续传(Range请求)熔断: 推荐服务挂了 → 返回全局热门歌单(降级)降级: 搜索服务挂了 → 显示预缓存的热门搜索结果限流: 每用户搜索QPS限制(防爬虫);下载速率限制(非付费用户)6. 可观测性
关键指标:
- 播放成功率(>99.9%)
- 播放启动延迟P99(<2秒)
- 搜索延迟P99(<200ms)
- 推荐CTR(点击率)
- 缓冲率(播放中卡顿的比例,<0.1%)
告警规则:
- 播放成功率<99% → P0告警
- 搜索延迟P99>500ms → P1告警
- CDN命中率<90% → P2告警
7. 演进路径
V1单体:所有功能在一个Go/Java应用中,PostgreSQL + 本地文件存储 V2模块化:播放模块、搜索模块、推荐模块明确分离,引入Redis缓存和S3存储 V3微服务:播放服务(高可用优先)、搜索服务(Elasticsearch)、推荐服务(ML管道)独立部署
练习2:知识点×系统矩阵填写
Section titled “练习2:知识点×系统矩阵填写”选择以下3个知识点,分别思考它们在11个系统中的具体应用方式,填写你自己的详细矩阵:
- 幂等性设计
- 缓存策略
- 数据分区
对于每个知识点×系统的组合,写出1-2句话说明具体如何应用。
点击查看解析(部分示例)
幂等性设计在各系统中的应用:
| 系统 | 幂等性应用 |
|---|---|
| URL Shortener | 同一长URL始终返回同一短码(通过URL哈希去重) |
| Web Crawler | 同一URL重复爬取不会创建重复的搜索索引条目 |
| News Feed | 同一帖子的点赞操作:多次点赞=一次点赞(UPSERT) |
| Chat System | 客户端消息ID去重:网络重试不会产生重复消息 |
| Search Engine | 搜索请求天然幂等(只读操作) |
| YouTube | 观看计数去重:同一用户短时间内多次刷新不重复计数 |
| Google Drive | 文件上传中断后续传:通过分片编号保证不重复写入 |
| Proximity Service | 位置更新幂等:相同时间戳的位置更新覆盖而非追加 |
| Google Maps | 路线规划请求天然幂等(只读) |
| Hotel Reservation | 支付回调通过payment_id去重;预订请求通过idempotency_key去重 |
| Gaming Leaderboard | 分数上报通过(玩家ID, 游戏场次ID)去重 |
缓存策略在各系统中的应用:
| 系统 | 缓存什么 | 策略 | TTL |
|---|---|---|---|
| URL Shortener | 短码→长URL映射 | Cache-Aside | 24h(热门链接) |
| News Feed | 用户的Feed列表 | Write-Behind | 无TTL(事件驱动失效) |
| Chat System | 用户在线状态 | Write-Through | 30s |
| Search Engine | 热门查询结果 | Cache-Aside | 5min |
| YouTube | 视频元数据+缩略图 | CDN+Cache-Aside | 1h |
| Google Maps | 地图瓦片 | 多层缓存 | 30天 |
| Hotel Reservation | 酒店房间价格 | Cache-Aside | 5min+主动失效 |
| Gaming Leaderboard | Top 100排行榜 | Write-Through | 10s |
这个练习的价值:迫使你从”知识点视角”而非”系统视角”思考问题。当你能流畅地说出”幂等性在12个不同场景中的应用”时,你对幂等性的理解就超越了教科书定义。
本模块核心收获:系统设计是一个完整的知识体系——从架构演进(单体→微服务)到技术选型(数据库决策树)到性能优化(多层缓存)到可靠性保障(故障设计四板斧)到运维保障(可观测性三支柱)。没有放之四海而皆准的方案,只有在具体场景下的最优权衡。知识检查清单和知识矩阵是帮你在面试和实际工作中系统性思考的工具。
整个V3课程的终极目标:不是记住11个系统的设计方案,而是建立一个可以应用到任何系统的思维框架。当你面对一个从未见过的系统设计问题时,能够从需求分析开始,逐层推导出合理的架构——这就是系统设计能力。