Skip to main content

参考:常见系统设计决策

一份可快速查阅的常见系统设计选择清单——存储、性能、架构、可靠性——每一项都给出默认选择以及何时应该切换。

它是什么

一份速查清单,汇集了几乎每道系统设计题都会反复出现的架构决策。每一项都给出一个默认选择以及应该让你切换的情形——它是 系统设计中的权衡思考 中推理过程的快速复习伴侣。

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 feedPush vs pull 扇出Push(名人用混合)
Chat连接模型 + 排序WebSocket + 每会话序列号
Rate limiter计数算法 + 存储Redis 中的 token bucket
分布式 KV 存储一致性 + 分区Leaderless quorums + 一致性哈希
订单处理多步正确性带补偿的 saga
限时抢购库存上的写争用强一致性 + 原子递减
广告点击聚合投递语义 + 窗口化至少一次 + 幂等、tumbling window
指标/监控存储 + 基数限制Time-series 存储;限制 label 基数
搜索索引结构 + 新鲜度Inverted index;近实时刷新
地理空间”附近”空间索引Geohash/H3、9 格扫描
文件存储/同步冲突处理内容寻址 + 增量同步

相关