CAP 定理:分布式的取舍三角
面试官抛出一个经典问题:设计一个跨机房的订单系统,一致性和可用性你选哪个?你答"我都要"。对方笑而不语。
再问:为什么 ZooKeeper 一选主,整个集群就拒绝写入,而 Eureka 明明发现一半节点失联了,还照样对外返回服务列表?为什么 Redis Cluster 默认一个槽位不可用就整集群报 CLUSTERDOWN?为什么 MySQL 主从复制可以在超时后"降级"成异步?
这四个问题背后是同一个东西——CAP 定理。它大概是分布式领域被引用最多、也被误读最多的一条原则。这一篇把它从猜想讲到定理,再讲到十二年后作者本人的纠偏,最后落到真实的选型现场。
一、它从哪来
CAP 不是一开始就叫"定理",它最初是个猜想。
- 2000 年 7 月,UC Berkeley 的 Eric Brewer 在 ACM 的 PODC 会议(Principles of Distributed Computing)上做主题演讲,提出一个判断:一个分布式系统无法同时满足**一致性(Consistency)、可用性(Availability)、分区容忍(Partition tolerance)**这三条。当时没有证明,他称之为 "CAP Conjecture"。
- 2002 年,MIT 的 Seth Gilbert 和 Nancy Lynch 把这条猜想形式化证明了,论文标题很长但意思很直白——《Brewer 的猜想与一致、可用、分区容忍的 Web 服务之可行性》。至此 CAP 从"行业直觉"变成了"可证明的结论"。
- 2012 年,Brewer 自己写了一篇《CAP Twelve Years Later: How the "Rules" Have Changed》纠偏。他说当年那句"三选二"被简化得走了样:P 不是一道选择题,而是分布式系统的既定前提;真正的选择只在 C 与 A 之间,而且只在分区发生的那段时间里才需要做。
这三步很关键——很多人脑子里的 CAP 停留在 2000 年的"三选二",而工程上真正有用的是 2012 年的版本。
二、为什么需要它
先把三个字母的严格定义摆清楚,这是全文的基石(日常说法和学术定义差得很远):
| 字母 | 含义 | 严格定义 |
|---|---|---|
| C 一致性 | 所有节点看到同一份数据 | 等价于线性一致性:一次写完成后,任何节点的读都必须看到这次写(或更新的值) |
| A 可用性 | 每个请求都能得到响应 | 每个非故障节点收到的请求,都必须在有限时间内返回非错误响应 |
| P 分区容忍 | 网络断了系统还能转 | 网络中任意数量的消息丢失或延迟时,系统仍能继续运行 |
注意 A 的定义有多苛刻:返回 503 也算不可用。一个系统"还能访问、只是报错",在 CAP 的语境里不算保住了 A。
那为什么必须做取舍?因为分区不是选项,是物理事实。光纤会被挖断、交换机会重启、云厂商的可用区会抖动、机器会因为一次长 GC 停顿几十秒被误判为宕机、丢包本来就是网络的常态。你无法承诺"我的网络永不断"——所以 P 是前提,不是筹码。
分区一旦发生,矛盾就摆到台面上。假设两个节点 A、B 被网络割开,客户端分别往两边写同一个变量:
- 如果两边都照常响应(保 A),那 A 节点上是
x=1、B 节点上是x=2,两份数据不一致——丢了 C。 - 如果两边都要保证一致,那么至少有一边必须拒绝写入或拒绝读取(保 C),请求得不到正常响应——丢了 A。
本质一句话:CAP 定理 = 网络分区一旦发生,一致性与可用性只能二选一;"三选二"是误读,P 是前提不是选项。
三、一张图看懂
把三个字母重新摆一次位置,就能看出"三选二"错在哪:
四、它有什么用
CAP 不是纸上的概念,它是数据库与中间件选型的第一性问题。四个真实场景:
1. CP 阵营:etcd / ZooKeeper —— 宁可不可用,不给错数据
etcd 用 Raft 做多数派复制。网络一断,少数派那一侧的节点拿不到多数票,直接拒绝写入:
1 | # 正常时 |
ZooKeeper 更极端:一旦进入选主阶段,整个集群会拒绝所有写入请求。这就是为什么注册中心、配置中心、分布式锁这些"绝对不能读到脏数据"的场景偏爱 ZooKeeper/etcd——它们把 C 看得比 A 重。
2. AP 阵营:Eureka —— 宁可返回过期注册表,也不让服务发现瘫痪
Eureka 有个著名的自我保护模式(self-preservation):如果 15 分钟内收到的心跳续约数低于期望值的 85%,它就不再剔除任何实例,同时对外返回可能已经过期的服务列表。
1 | # eureka.server 侧:低于阈值就停止剔除实例 |
这是 AP 取舍的教科书样本:"服务列表里可能有个已经挂掉的实例"(丢一点 C)远比**"整个服务发现彻底不可用、所有调用方拿不到任何地址"**(丢 A)要好得多。当年 Spring Cloud 生态里 Eureka 与 ZooKeeper 之争,本质就是 AP 与 CP 之争。
3. 可调阵营:Cassandra —— 同一次请求现场选边
Cassandra 把取舍做成了可调参数。设副本数 N=3,读写各要几个副本应答由你定:
1 | -- 写入要 2 个副本确认,读取也要 2 个副本确认 |
R + W > N 时读到最新数据,等于往 CP 侧挪;设成 ONE 则完全倒向 AP。CAP 不是给系统贴的永久标签,而是可以按请求逐次选择的旋钮——这是 2012 年之后最被低估的一点。
4. 滑动档位:Redis Cluster 与 MySQL 半同步
- Redis Cluster 的
cluster-require-full-coverage yes(默认):只要有任何一个哈希槽不可用,整集群就返回CLUSTERDOWN,CLUSTER INFO里显示cluster_state:fail——选 C 弃 A。把它改成no,集群就允许部分槽位失效仍对外服务——倒向 AP。 - MySQL 半同步复制:主库提交后要等至少一个从库 ACK 才返回。如果等待超时,
rpl_semi_sync_master_timeout(默认 10 秒)一到就自动降级为异步复制,先让业务跑起来,代价是从库可能落后——这是从 C 滑向 A 的实时降级。
五、反例与边界
CAP 被滥用得厉害,四条边界值得划清:
- "三选二"是最大的误读。P 不是你能放弃的选项——只要跨网络部署,分区迟早会发生。所以真正的形态是"分区发生时,C 与 A 二选一",而不是"从三个里挑两个"。宣称自己"CA 系统"的分布式产品,要么是在分区时不诚实,要么是把 P 定义偷偷放宽了。
- 选择是"分区期间"的临时状态,不是永久人设。绝大多数系统在没有分区时同时保 C 和 A。CAP 只在分区窗口内生效。分区结束后,AP 系统要做反熵(anti-entropy)、读修复(read repair)、冲突合并,把两侧的分歧收敛掉——选了 A 不等于永远不要 C,只是把"当下的一致"推迟成"事后的一致"。
- CAP 完全不谈延迟。它只管分区。可现实里 99.99% 的时间网络是通的,此时真正的取舍是"延迟 vs 一致性"。于是有了 PACELC:分区时(P)选 A 还是 C;否则(E, Else)选 L(延迟)还是 C。MySQL 异步复制平时就是在选 L——低延迟,但可能读到旧数据。CAP 解释不了的日常问题,PACELC 才管。
- CP/AP 是光谱,不是二分。Cassandra 的
LOCAL_QUORUM可以做到"分区内部保 C、跨分区保 A";很多系统在分区时的表现既不是纯 C 也不是纯 A,而是按数据分片、按请求类型分别处理。把架构决策简化成"我们是 CP 还是 AP",往往是因为还没想清楚到底哪些数据不能脏、哪些请求不能等。
六、对比表与小结
| 维度 | CP(保一致弃可用) | AP(保可用弃一致) |
|---|---|---|
| 分区时的行为 | 少数派侧拒绝读写 | 两侧各自读写,产生分歧 |
| 用户体验 | 部分请求失败/超时 | 全部有响应,可能读到旧值 |
| 典型系统 | etcd、ZooKeeper、HBase、MySQL 半同步 | Cassandra、DynamoDB、Eureka、Nacos(AP) |
| 适合场景 | 配置中心、分布式锁、选主、账户余额 | 服务发现、购物车、社交动态、监控指标 |
| 分区恢复后 | 直接一致(本来就没分歧) | 需反熵/读修复/冲突合并才能收敛 |
| 核心代价 | 可用性受损,故障被放大到业务 | 复杂度转移,一致性逻辑要业务自己扛 |
🐾 小结:CAP 定理的价值不在于告诉你"选两个",而在于逼你把话说清楚——当网络真的断了,你的系统到底允许哪一部分坏掉? 配置中心断了不该读到脏配置,所以它选 C;服务发现断了不该全体瘫痪,所以它选 A。真正成熟的架构不是回避这个选择,而是按数据的性质分类:哪些字段宁可不可用也要保一致,哪些字段宁可旧一点也要有响应。想清楚这件事,选型表上的每一个勾,才不是背出来的,而是算出来的。
相关阅读
- 幂等性:让重试变得安全(分区恢复后的重试与补偿,靠的就是幂等):/posts/princ-idempotency/
- 端到端原则:TCP 为什么把可靠性放在两端("网络不可靠"正是同一条前提):/posts/princ-end-to-end/

