它是什么
一份速查清单,汇集了几乎每道系统设计题都会反复出现的架构决策。每一项都给出一个默认选择以及应该让你切换的情形——它是 系统设计中的权衡思考 中推理过程的快速复习伴侣。
Note
这里的”默认”是什么意思: 指最安全的面试起点——那个很少出错、容易辩护的选择——而非放之四海皆准的最佳设计。真正的能力在于知道何时情形会把你推离默认,这正是每条”→ 切换到”想要点明的。
何时需要它
面试当晚复习时用它,或当你已经点明了某个决策点、想一眼看到标准答案时用它。框架文章教你如何推理出一个选择;这篇讲的是有哪些常见选择,好让你快速识别出切换的时机。
它防范的陷阱:只背默认选择却说不出触发条件。面试官不想听到”用 Redis”——他们想听到”默认 cache-aside,但因为这里写多,改用 write-behind”。把每一项读作默认 → 当情形变化时,切换到 → 为什么,并把切换大声说出来。
存储引擎
数据如何建模以及如何物理存储。
SQL vs NoSQL
- 默认:关系型(Postgres/MySQL)。 你需要事务、join,或者访问模式还没固定下来。除非有具体的压力把你推离,否则从这里开始。
- 已知在规模下的单键访问 → key-value / wide-column(DynamoDB、Cassandra)。 当每个查询都是”按这个键”、永远不需要 join 时,关系型的开销买不到任何东西。
- 写多的摄入(time-series、事件、日志)→ LSM-tree 存储(Cassandra、Scylla、RocksDB)。 顺序追加写胜过 B-tree 的随机写。
- 读多且 join 可预测 → 关系型 + read replicas + 缓存。 在换用不同数据模型之前,先横向扩展读。
- 常见错误: 为了”扩展性”选了 NoSQL 存储,却仍然需要多行事务——你会在应用层里蹩脚地重建它们。
完整细节:SQL vs NoSQL Schema 设计、数据库索引。
存储架构
存储的数据如何分布、复制并保持一致。
分区策略
- 默认:hash partitioning。 在各 shard 间均匀分布,无热点——当你只按键查询时的安全选择。
- 你需要范围扫描(时间范围、字母序)→ range partitioning。 让相邻数据聚在一起,一次扫描只命中少数 shard。代价:热点范围的风险。
- 随着增长要重新分区 → 一致性哈希。 增删一个节点只移动
1/N的键,而非重新洗牌全部。这正是”如何在不停机的情况下增加容量?“那个答案。 - 常见错误: 某个键主导了流量(热点分区)——给键加盐或拆分它。参见 NoSQL 热点键缓解。
完整细节:分区策略。
复制策略
- 默认:single-leader(primary-replica)。 简单、无写冲突、推理容易。读通过 replica 扩展;写受 leader 限制。
- 写吞吐超过单个 leader,或你需要多区域写 → multi-leader / leaderless。 换来写扩展和局部性;代价是冲突解决。
- 可用性优先于新鲜度 → leaderless quorums(Dynamo 风格)。 调整 R + W > N 以匹配你想要的读写平衡。
- 常见错误: 承诺从异步 replica 得到强一致读——follower 总可能落后于 leader。
完整细节:复制策略。
一致性模型
- 面向用户数据的默认:最终一致 / read-your-writes。 可用性和延迟比每个 replica 瞬间一致更重要。社交信息流可以有几秒的陈旧。
- 金钱、库存、唯一性 → 强一致性(linearizable)。 任何”错但快”比”慢”更糟的场景。付出协调延迟。
- 多区域、分区容错 → 按 CAP/PACELC 情形选择。 分区发生时你在可用性和一致性之间选;即使没有分区,你也在延迟和一致性之间选。
- 常见错误: 到处都默认强一致性——你在从不需要它的数据上付出了协调延迟。
完整细节:一致性模型。
性能
如何在不改变核心架构的前提下达到延迟和吞吐目标。
缓存策略
- 默认:cache-aside + TTL。 读是常规的,短暂陈旧可以接受。应用读缓存,miss 时回退 DB,再回填。短 TTL 限定一个值能陈旧到什么程度。
- 需要 read-after-write → write-through。 写同时更新缓存和 DB,这样读者永远看不到比自己的写更旧的值。代价是每次写的延迟。
- 写多、可容忍丢失 → write-behind。 在缓存中缓冲写,异步刷入 DB。快,但崩溃会丢失未刷入的窗口——只用于计数器、指标、session 数据。
- 常见错误: 一个热点单键(名人、限时抢购商品)同时过期——加 single-flight 或逻辑过期以防止缓存击穿的踩踏。
完整细节:缓存访问与失效模式。
异步处理
- 默认:同步。 调用方现在就需要结果,或者工作很轻。最容易推理和调试。
- 慢的、可重试的或扇出的工作 → 通过队列异步。 扇出(通知、信息流写入)、任何用户不该等待的工作。将调用方延迟与工作解耦。
- 精确一次的问题 → 至少一次投递 + 幂等消费者。 真正的精确一次代价高昂;至少一次加一个幂等键才是标准答案。
- 常见错误: 对调用方实际需要同步的工作走异步——你白白增加了一个队列和最终一致性。
完整细节:队列投递语义。
限流
- 默认:token bucket。 允许突发直到桶的大小,然后稳定补充——匹配 API 实际想要的行为方式。
- 平滑、无突发 → sliding-window counter。 更严格、更均匀。
- 跨节点分布式 → Redis 中的集中计数器(用 Lua 保证原子性)。 仅本地的计数器会让客户端超过全局限制。
- 常见错误: 每节点本地计数器——N 个节点意味着实际全局限制的 N 倍。
完整细节:限流算法。
架构
服务如何通信、协调,并跨进程边界保持正确。
分布式事务模式
- 跨服务的默认:saga(补偿)。 每一步都有一个撤销;失败时向后运行补偿。因为不持有跨服务锁,所以能扩展。
- 你控制所有参与者且能短暂持锁 → 2PC。 更强的原子性,但被阻塞的协调者会拖住所有人——只在紧凑、可控的边界内可行。
- 单个数据库 → 一个普通的 ACID 事务。 当一个 DB 的事务已经给你原子性时,别去够分布式协议。
- 常见错误: 在微服务间够 2PC——被阻塞的协调者故障模式比 saga 的最终一致性更糟。
完整细节:分布式事务模式。
通信模式
- 默认:同步请求/响应(REST/gRPC)。 直接、易于追踪,当调用方需要一个答案才能继续时是对的。
- 生产者不该等待消费者 → 异步消息(队列/日志)。 解耦服务并吸收突发;代价是最终一致性和更难的追踪。
- 一个事件、多个感兴趣的服务 → publish/subscribe。 把单个事件扇出给独立消费者,而非点对点调用。
- 常见错误: 跨多个服务的同步调用链——一个慢依赖拖住整个请求并耦合了它们的可用性。
完整细节:API 设计模式。
实时推送
- 默认:WebSocket 用于真正的双向(聊天、协作)。
- 仅服务器→客户端 → SSE。 实时信息流、通知——当客户端不推送时比 WebSocket 更简单。
- 遗留/兼容性 → long-poll。 当 WebSocket/SSE 不可用时的兜底。
- 常见错误: 高扇出(数百万订阅者)直接推送——应在生产者和连接之间放一个 pub/sub 层。
完整细节:实时连接模式。
可靠性
系统如何在部分故障中存活——几乎每个设计最终都会需要的决策。
重试与退避
- 默认:带指数退避 + jitter 的重试。 瞬时故障(抖动、短暂过载)会自行恢复;退避避免猛击一个挣扎中的依赖,jitter 避免同步的重试风暴。
- 只重试幂等操作 → 否则配一个幂等键。 重试一个非幂等写会重复扣款、重复发货。
- 常见错误: 无上限的固定间隔重试——它们把一次依赖抖动变成自己制造的 重试风暴。
幂等
- 默认:每个客户端发起的变更配一个幂等键。 服务器按键去重,这样重试的请求只应用一次。这正是使至少一次投递安全的东西。
- 存在天然幂等性 → 用它。
SET x = v或按唯一字段键控的 upsert 不需要额外的键。 - 常见错误: 反而假设精确一次投递——按至少一次 + 幂等来构建;真正的精确一次在传输层是一个网络神话。
超时与熔断器
- 默认:每个远程调用都加超时。 没有超时意味着一个挂起的依赖会耗尽你的线程/连接池并级联。
- 对某个依赖反复失败 → 熔断器。 超过失败阈值后跳闸打开、快速失败、定期探测——阻止一个死掉的依赖把你拖垮,并给它恢复的空间。
- 常见错误: 因为”在同一个数据中心”就省略内部调用的超时——内部调用也会挂起,而那些正是拖垮整个系统的级联。
死信队列
- 默认:为反复失败的消息配一个死信队列。 在 N 次处理失败后,把消息挪到一边,这样一条坏的(“毒丸”)消息不会阻塞整个分区。
- 需要恢复它们 → 让 DLQ 可重放。 修复 bug,然后从 DLQ 重新驱动。
- 常见错误: 对毒丸消息原地无限重试——它会拖住分区里排在它后面的每一条消息。
一览:问题 → 它取决于的决策
一张用于复习的回忆网格。每一行是一个常见问题以及它真正在考察的那一个决策——你该花推理力的地方。用上面各小节标注的决策已在此编录;其余的(ID 生成、窗口化、空间索引、内容寻址)在各自的演练中覆盖,是未来条目的候选。
| 问题 | 它取决于的决策 | 默认选择 |
|---|---|---|
| URL shortener | 缓存放置 + ID 生成 | Cache-aside、长 TTL;counter/base62 ID |
| News feed | Push vs pull 扇出 | Push(名人用混合) |
| Chat | 连接模型 + 排序 | WebSocket + 每会话序列号 |
| Rate limiter | 计数算法 + 存储 | Redis 中的 token bucket |
| 分布式 KV 存储 | 一致性 + 分区 | Leaderless quorums + 一致性哈希 |
| 订单处理 | 多步正确性 | 带补偿的 saga |
| 限时抢购 | 库存上的写争用 | 强一致性 + 原子递减 |
| 广告点击聚合 | 投递语义 + 窗口化 | 至少一次 + 幂等、tumbling window |
| 指标/监控 | 存储 + 基数限制 | Time-series 存储;限制 label 基数 |
| 搜索 | 索引结构 + 新鲜度 | Inverted index;近实时刷新 |
| 地理空间”附近” | 空间索引 | Geohash/H3、9 格扫描 |
| 文件存储/同步 | 冲突处理 | 内容寻址 + 增量同步 |
相关
- 框架:系统设计中的权衡思考 — 产生这些选择的推理过程:轴 → 上下文 → 成本。先读它;把这份清单当查阅表用。
- 参考:一致性模型 — 上面一致性那一行的深入版本。
- 参考:缓存访问与失效模式 — 缓存和可靠性各行的深入版本,包括重试风暴和踩踏故障模式。
- 参考:队列投递语义 — 可靠性小节背后的至少一次、幂等和死信处理。