CAP 定理是分布式系统里最有名也最被误解的理论。"CAP 三选二" 这个口诀人人会背,但真要解释"为什么不能同时满足"、"P 不能放弃是什么意思"、"NoSQL 是 AP 还是 CP"、"BASE 和 ACID 怎么权衡",大多数工程师就开始打结。这篇文章把 CAP / PACELC / BASE 一次讲透,所有结论都配真实系统案例。
CAP 定理的精确表述
Eric Brewer 2000 年提出,2002 年由 Gilbert 和 Lynch 形式化证明:
在一个分布式系统里,当网络分区(P)发生时,你只能在一致性(C)和可用性(A)之间二选一。
三个字母:
- Consistency(一致性):所有节点看到的数据都是同步且最新的。
- Availability(可用性):每个请求都能在合理时间内得到非错误响应。
- Partition Tolerance(分区容忍性):节点之间的网络出问题(部分消息丢失或延迟)时,系统依然能继续工作。
为什么不能 CAP 全要
想象一个简单场景:节点 A 和节点 B 各存了一份数据 X = 5。某一时刻 A 和 B 之间网络中断(分区发生):
用户 1 -> A: 写 X = 10
用户 2 -> B: 读 X
# 这时 A 和 B 通不了,A 已经 X=10,但 B 还是 X=5
# 系统怎么响应用户 2?
选 C(一致性):B 必须拒绝服务或返回错误,等网络恢复同步后再服务。
→ 牺牲了 A(可用性)
选 A(可用性):B 直接返回 X=5,即使这个值已经过时。
→ 牺牲了 C(一致性)
如果说"我两个都不选,直接关掉分区"?那就放弃了 P。
但 P 是分布式系统的物理现实 —— 网络不可能永远完美。
P 不能放弃
这是 CAP 最容易被误解的点。在分布式系统里,P 不是你想不想要的问题,而是迟早会发生的物理事实。光速有限、机房断电、网卡故障、TCP 超时、跨数据中心延迟 —— 这些不可控因素都会造成节点间通信受阻。
所以真正的选择是:当 P 发生时,你选 C 还是 A。单机数据库(MySQL)可以做到 CA,因为它根本没有分布式 —— 不存在 P 的问题。一旦你做多副本同步、跨机房部署,P 就必然存在。
CP 系统的代表:Zookeeper、etcd、HBase
CP 系统在分区发生时宁可不可用,也要保证数据一致。典型场景:
- 配置中心:Zookeeper / etcd,丢一致比丢可用糟糕得多 —— 不同节点拿到不同配置,系统全乱。
- 分布式协调:Zookeeper 选主、分布式锁。
- 金融账目:转账如果不一致(同一时刻余额不同),问题严重。
# etcd 的典型 CP 行为
client.put("config", "value") # 写入
# 内部:Leader 接收 -> 多数节点 ack -> 才回复成功
# 如果 Leader 和多数节点失联,整个集群不再接受写入(放弃 A)
AP 系统的代表:Cassandra、DynamoDB、Couchbase
AP 系统在分区发生时宁可数据短暂不一致,也要保证持续可用。典型场景:
- 大型电商商品目录:不同节点价格短暂不同(几秒)可接受,但页面不能打不开。
- 社交动态:你刚发的帖子在某个数据中心立刻可见,在另一个 10 秒后才可见,不影响体验。
- 购物车:Amazon 的 DynamoDB 论文里讲过 —— "购物车永远不能拒绝加购",哪怕分区后两边都加了,稍后合并。
# Cassandra 的典型 AP 行为
write(key, value, consistency=ONE) # 任一节点写成功就返回
# 即使其他节点暂时拿不到,以后 "Anti-Entropy" 后台同步
"最终一致性":AP 系统的妥协
AP 系统不是"永远不一致",而是"暂时不一致,但最终会收敛"。这个性质叫最终一致性(Eventual Consistency)。实现技术:
- 反熵(Anti-Entropy):后台定期对比节点数据,把不一致的同步过来。
- Read Repair:读取时发现多副本不一致,顺便修复。
- Hinted Handoff:写入时目标节点不可达,把数据暂存到其他节点,等它恢复时转交。
- Vector Clock:用向量时钟标记每个版本,合并时按规则选最新。
Dynamo / Cassandra 都用这套机制。最终一致性的"最终"通常是几秒到几分钟,大多数应用场景能接受。
PACELC:CAP 的扩展
CAP 只讨论"分区发生时"的权衡,但实际系统大多数时间都没分区。Daniel Abadi 提出 PACELC 扩展:
Partition 时,在 Availability 和 Consistency 间选;
Else(无分区时),在 Latency(延迟)和 Consistency 间选。
无分区时为什么也有权衡?因为强一致性需要节点间通信(等多数确认),通信本身有延迟。选强一致 = 接受更高延迟;选低延迟 = 牺牲一些一致性。
系统 PACELC 选择
MongoDB PA/EC 分区时选 A(降级到本地副本),无分区选 C(等多数确认)
Cassandra PA/EL 两种情况都偏向 A 和 L
DynamoDB PA/EL 同上
HBase PC/EC 两种情况都偏向 C
PNUTS (Yahoo) PC/EL 分区时 C,无分区时 L
这比单纯说"AP 或 CP"细致得多。选型时看 PACELC 对你日常 SLA 影响更直接。
BASE:AP 系统的设计哲学
对照 ACID(Atomicity、Consistency、Isolation、Durability),BASE 是 AP 系统的设计原则:
- Basically Available:基本可用(可能性能下降、部分功能可用)。
- Soft State:中间状态被允许,不强求时刻一致。
- Eventual Consistency:最终一致。
BASE 是对 ACID 的对立 —— 互联网大规模系统选了 BASE,因为"规模、可用性、弹性"比"强一致"更重要。但金融、医疗等场景仍然需要 ACID。不是 BASE 一定比 ACID 好,而是根据业务选。
一致性的细分
"一致性"本身有很多级别,不是"强一致"和"弱一致"两种:
强一致性(Strong Consistency / Linearizability)
任何读总能看到最近的写。语义上等同于单机系统。Zookeeper、etcd、Spanner 提供。代价:延迟高,可用性低。
顺序一致性(Sequential Consistency)
所有节点看到的操作顺序是同一个,但不一定是真实时间顺序。
因果一致性(Causal Consistency)
有因果关系的操作必须保序,无因果关系的可以不同顺序看。COPS、CRDTs 实现。
读自己写一致(Read Your Writes)
用户自己刚写的数据,自己一定能读到。其他用户可能稍后看到。
单调读一致(Monotonic Reads)
读了一个值之后,后续读不能"变回旧版本"。
最终一致(Eventual Consistency)
最弱的实用级别 —— 写入后无新写,系统最终会让所有副本收敛到同一值。
真实系统的设计权衡
MySQL 主从
主库强一致(单机 ACID),从库异步复制 —— 副本最终一致。读从库可能读到旧数据(几秒延迟)。这是典型的"主写从读,接受复制滞后"模式。要严格一致就读主,要扩展就读从。
MongoDB 副本集
有 "majority" / "linearizable" 读写关注级别(Read/Write Concern),让你在不同操作上选不同一致性。一个集群里写关注 majority(强一致)和写关注 1(快速)可以共存。
Redis Cluster
分片 + 主从异步复制。默认 AP,故障切换时可能丢几秒数据。要强一致用 Redis Sentinel + 同步复制(WAIT 命令)。
Spanner / TiDB
Google Spanner 和 TiDB 这类"NewSQL"号称"CP + 高可用":用 Raft 是它的简化版。">Paxos / Raft 保证多数副本可用即可服务,且强一致。但需要原子钟 / TrueTime / NTP 等基础设施支持。
常见误解
误解 1:"NoSQL 都是 AP"。错。HBase / MongoDB(默认配置)都是 CP,Cassandra / DynamoDB 是 AP。看实现,不看大类。
误解 2:"AP 系统数据会丢"。错。AP 是说"暂时不一致",不是"数据丢失"。可靠性(数据不丢)由 Durability 保证,和 CAP 是另一个维度。
误解 3:"CAP 只在数据库选"。错。CAP 是任何分布式状态系统的约束 —— 缓存、消息队列、配置中心、服务发现都要面对。
误解 4:"选了 A 就完全没一致性"。错。AP 系统通过最终一致性、Quorum、CRDT 等技术让不一致窗口尽可能小。
误解 5:"PACELC 替代了 CAP"。PACELC 是 CAP 的扩展和细化,两者描述的是同一个本质 —— "一致性与其他属性的权衡"。
工程上怎么用 CAP 思考
设计分布式系统时,问自己:
- 这块数据,不一致几秒会怎样?(评估对 AP 的容忍度)
- 这块数据,读不到几秒会怎样?(评估对 CP 的容忍度)
- 读写比例如何?(读多写少,适合 AP + 多从读)
- SLA 要求几个 9?(高可用必然偏 AP)
- 跨数据中心吗?(跨 DC 时分区频率高,P 必须考虑)
同一个系统里不同数据可以选不同策略:用户余额选 CP,商品评论选 AP,购物车选 AP + 冲突合并。把"CAP 选择"做到数据维度,而不是系统维度。
CRDT:不需要协调的最终一致
CRDT(Conflict-free Replicated Data Type,无冲突复制数据类型)是 AP 系统里的明星技术。多个副本独立修改,合并时数学上保证一致,不需要中央协调。
# Counter CRDT(G-Counter)
class GCounter:
def __init__(self, node_id, nodes):
self.node_id = node_id
self.counts = {n: 0 for n in nodes}
def increment(self):
self.counts[self.node_id] += 1
def value(self):
return sum(self.counts.values())
def merge(self, other):
for n in self.counts:
self.counts[n] = max(self.counts[n], other.counts[n])
# 两个节点各自 +5,合并后总值 10,不论合并顺序
a = GCounter("A", ["A", "B"])
b = GCounter("B", ["A", "B"])
for _ in range(5): a.increment()
for _ in range(5): b.increment()
a.merge(b)
print(a.value()) # 10
CRDT 有 Counter、Set(添加/移除)、Map、Register、JSON 等多种。Riak、Akka Distributed Data、Yjs(协作编辑)、Automerge 都用 CRDT。它把"多副本最终一致"从工程难题变成了数学性质 —— 不需要 vector clock 比较、不需要 last-write-wins,合并函数本身就是单调的。
Quorum 配置:NWR 的工程艺术
AP 系统通常允许配置读写副本数。三个参数:
- N:总副本数。
- W:写入要等几个副本 ack。
- R:读取要查几个副本(取最新)。
如果 R + W > N,读写集合必有交集 → 强一致。反之就是最终一致。
N=3, W=2, R=2 -> 强一致,W+R=4>3 必有交集
N=3, W=3, R=1 -> 写全副本(慢)、读任一(快)
N=3, W=1, R=1 -> 写快读快,但弱一致
N=5, W=3, R=3 -> 多副本+强一致,典型 Cassandra 配置
Cassandra / DynamoDB 都让你按操作选 W、R,极其灵活 —— "账户表 W=3 R=3,日志表 W=1 R=1"。这就是把 CAP 权衡做到 API 粒度。
真实分区案例的处理
2017 年 AWS S3 us-east-1 大故障源于一个工程师的手误,但事故里 S3 的元数据子系统不可用 → 间接拖垮了 Netflix、Slack、GitHub 等大批服务。事后 AWS 加强了"区域隔离":一个 AZ 挂掉,其他 AZ 必须能独立工作。这就是 P 容忍的实战 —— 不仅要"系统能容忍分区",还要"分区不让其他部分一起塌"。这也是云架构师设计"Multi-AZ"的根本动机。
"读自己写"的工程实现
主从架构里,用户刚写完读不到的体验差。常见解决:
- 读主:写完后短时间内强制读主库,几秒后再切回从。简单粗暴。
- 会话粘性:同一用户的请求路由到同一节点。
- 读后写自己看到:客户端记住写入时的 timestamp / version,读时要求"不低于这个版本"。MongoDB 的
readConcern: "majority"+ casualConsistency 实现。
面试常考:几个具体系统的 CAP 归类
面试官常问"X 是 CP 还是 AP",答案往往不是简单二选一,而是"可配置 / 默认配置下的表现":
MySQL 主从复制
主库本身是单机 ACID,不在 CAP 范畴。主从异步复制 = 从库最终一致(AP),同步复制 = 强一致(CP,但主库压力大)。MySQL 5.7+ 的"半同步复制"(semi-sync)是折中:至少一个从库确认才提交,典型的"多数派"思想雏形。
Kafka
Kafka 默认 AP(异步复制 + acks=1 写主即返回)。配置成 acks=all + min.insync.replicas=2 后变 CP —— 但代价是任一副本不可用时整个 partition 不能写。多数生产用 acks=1,接受少量数据丢失换可用性。
Zookeeper
严格 CP。Leader 选举失败时整个集群不可用。这正是它适合做"配置中心 / 协调服务"的原因 —— 这些场景宁可不可用也不能让不同客户端拿到不同配置。
Redis Sentinel
故障切换时短暂不可用 + 可能丢数据(异步复制),综合是 AP 偏向。但 Redis 单实例自己是 CP(单点强一致,不存在分区)。
MongoDB
很灵活。默认 PA/EC(分区时副本集进入只读,无分区时 majority 读写强一致)。可以通过 readPreference / writeConcern 调整成更 AP 或更 CP。
DynamoDB
典型 AP 系统(论文级别)。但近年加了"强一致读"选项 —— 牺牲一些可用性换强一致,看你的请求怎么发。这种"同一系统,按操作选 CAP"是现代分布式数据库的趋势。
"PACELC 的 EL"实战例子
无分区时,在延迟和一致性间选 —— 这是日常 SLA 经常碰到的:
- 读自己的写需求:用户刚发的帖子立刻自己能看到。如果走异步主从 + 读从,可能 100ms 后才同步过来,用户感受到"我刚发的帖子怎么没了"。解决:同一会话内读主、或用 stickiness 路由到同一从。
- 跨地域写入:用户在中国写,数据要立刻让美国读到 → 强一致 = 跨太平洋同步等待几百 ms;最终一致 = 用户体验快但其他地区可能短暂看不到。Google Spanner 用原子钟 + TrueTime 让全球强一致;Amazon DynamoDB Global Tables 用最终一致换全球低延迟。
面试时怎么聊 CAP
避免几个常见错误:
- 不要说"我们系统是 CP"。系统是配置的产物,不同 endpoint / 不同操作可能选不同。说清"哪个数据 / 哪个操作 / 哪种配置下是什么"。
- 不要把"CAP"和"ACID"混着说。ACID 是单数据库事务的属性,CAP 是分布式系统的权衡。一个 MySQL 主从架构既可以讲它的 ACID(主库内部),也可以讲它的 CAP(主从间)。
- 提到"最终一致"时,要量化时长。"几秒"和"几分钟"是完全不同的业务影响。监控复制延迟、设置告警阈值,这是工程师该有的具体感。
真实分区时的演练
设计完系统后,做一次"Chaos Engineering"演练:在线上(或预发)环境注入网络分区,观察:
- 系统是否按预期降级(AP)或拒绝服务(CP)?
- 分区恢复后,数据多久收敛?有没有冲突需要人工干预?
- 客户端的重试 / fallback 行为是否合理?
Netflix Chaos Monkey、Gremlin、阿里 ChaosBlade 都提供这类故障注入。"我以为系统能容忍 X"和"系统确实容忍了 X"之间有巨大鸿沟,只有演练能填平。
写在最后
CAP 不是"三选二"那么简单,它是一组指导你"在分布式系统中如何分配权衡"的原则。理解 CAP / PACELC / BASE 不会让你的系统自动变好,但会让你清楚自己在选什么。架构评审时,问"这个系统在分区时怎么表现",比讨论一堆配置参数有用得多。
给一个工程心得:大多数项目的"分布式问题"其实不需要 CAP 理论指导,因为它根本不分布式 —— 单机 MySQL + Redis 就解决了 90% 的需求。真正涉及 CAP 时,通常是规模已经大到"单机扛不住"。所以新项目不要一上来就上 Cassandra / TiDB / 多机房,先把业务跑起来,瓶颈来时再做分布式架构。设计模式如此,分布式选型更如此。
—— 别看了 · 2026