CAP 定理
2025 年双十一零点,某电商平台的库存服务出现了超卖问题。
库存服务是一个典型的分布式系统,多个节点各自管理一部分库存。用户下单时,需要在多个节点间协调扣减库存。零点高峰期,由于网络抖动,三个机房之间的网络出现了短暂的分区。问题随之暴露:
- 节点 A 认为库存充足,扣减成功
- 节点 B 认为库存充足,扣减成功
- 节点 C 认为库存充足,扣减成功
但实际库存只有 1 件,三次扣减都成功了。这就是 CAP 定理在生产环境中的真实体现。
【架构权衡】
CAP 定理不是理论游戏,而是分布式系统设计的铁律。在设计分布式系统时,你必须在 C、A、P 三者之间做出取舍。而现实中,由于网络分区不可避免(P 必须满足),我们实际上只能选择 CP 或 AP。
一、问题背景
1.1 真实事故场景
某金融支付系统,采用了多机房部署。架构设计时,团队选择了"强一致性"路线,所有交易必须实时同步到所有机房。
双十一期间,其中一个机房的网络设备故障,导致与其他机房的网络中断 30 秒。
正常状态:
[机房A] ←同步→ [机房B] ←同步→ [机房C]
↓
[用户下单] → 实时同步到所有机房 → 强一致
网络分区状态:
[机房A] ← ✗断开 ✗ → [机房B] ← ✗断开 ✗ → [机房C]
选择 CP 的系统:在分区期间,机房 B 和 C 无法同步,停止服务(拒绝写入),保证了数据一致性,但损失了可用性。
选择 AP 的系统:在分区期间,每个机房独立服务,接受了数据不一致的风险,但保证了可用性。
1.2 CAP 的定义
CAP 定理(Brewer's Theorem):
在一个分布式系统中,以下三个特性不可能同时满足:
┌─────────────────────────────────────────────────────────────┐
│ C (Consistency) │
│ 一致性:所有节点看到相同的数据 │
├─────────────────────────────────────────────────────────────┤
│ A (Availability) │
│ 可用性:每个请求都能收到响应 │
├─────────────────────────────────────────────────────────────┤
│ P (Partition Tolerance) │
│ 分区容错:系统能容忍网络分区 │
└─────────────────────────────────────────────────────────────┘
【架构权衡】
这里有一个关键误解:很多人以为 CAP 意味着"三选二"。但实际上,分区容错(P)是分布式系统必须具备的能力——因为网络故障不可避免。所以真实的抉择是:在保证 P 的前提下,选择 C 还是 A。
二、方案演进
2.1 CP 系统:牺牲可用性换取一致性
代表系统:ZooKeeper、HBase、MongoDB(副本集模式)、etcd
CP 系统特点:
分区发生前:
[A节点] ←实时同步→ [B节点] ←实时同步→ [C节点]
↓
数据完全一致
分区发生后(AB 连通,BC 断开):
[A节点] ←实时同步→ [B节点]
↓ ✗ 断开 ✗
[C节点]
↓
B 无法同步到 C
B 停止服务(拒绝写入)
C 也停止服务(拒绝写入)
系统整体不可用,但数据一致
典型案例:ZooKeeper 在网络分区时的行为
ZooKeeper 使用 ZAB 协议保证强一致性。当发生网络分区时:
- 如果 Leader 所在分区节点数不足半数,整个集群停止服务
- 客户端无法写入,直到网络恢复
// ZooKeeper 写入流程
// 写入必须经过 Leader,且需要多数节点确认
WriteRequest → Leader → [Follower1, Follower2] → 等待多数ACK → 提交
2.2 AP 系统:牺牲一致性换取可用性
代表系统:Cassandra、DynamoDB、CouchDB、Redis Cluster(哨兵模式部分场景)
AP 系统特点:
分区发生前:
[A节点] ←实时同步→ [B节点] ←实时同步→ [C节点]
↓
数据完全一致
分区发生后(AB 连通,BC 断开):
[A节点] ←实时同步→ [B节点]
↓ ✗ 断开 ✗
[C节点]
↓
C 独立运行,接受写入
可能产生数据冲突
但系统始终可用
典型案例:Amazon DynamoDB 的最终一致性
DynamoDB 允许多个副本异步同步,在网络分区时:
- 客户端可以在任何副本写入
- 副本间通过向量时钟或时间戳解决冲突
- 最终达到一致(可能需要几秒到几分钟)
2.3 方案对比
【架构权衡】
选择 CP 还是 AP,本质上是"业务场景"的决策:
- 金融支付:宁可不可用,也要保证数据正确 —— CP
- 社交 Feed:宁可展示稍旧的数据,也要保证页面秒开 —— AP
- 库存超卖:宁可拒绝订单,也不能超卖 —— CP
三、核心设计
3.1 为什么 P 必须满足?
分布式系统的三个假设:
1. 网络不是可靠的(The network is not reliable)
2. 延迟不是零(Latency is not zero)
3. 带宽不是无限的(Bandwidth is not infinite)
结论:网络分区(Partition)在分布式系统中必然发生
所以 P 不是可选项,而是必选项
3.2 一致性的不同级别
CAP 中的 C(强一致性)不是唯一的选择:
一致性级别金字塔:
强一致性(Linearizability)
↓
顺序一致性(Sequential)
↓
因果一致性(Causal)
↓
最终一致性(Eventual)
↓
弱一致性(Weak)
3.3 BASE 理论:CAP 的实际工程折中
BASE 理论 = Basically Available + Soft state + Eventually consistent
┌─────────────────────────────────────────────────────────────┐
│ Basically Available │
│ 基本可用:系统保证核心功能可用 │
│ 分区期间:可以降级、延迟响应、返回默认值 │
├─────────────────────────────────────────────────────────────┤
│ Soft state │
│ 软状态:数据状态可以不同步 │
│ 分区期间:各节点数据副本可能暂时不一致 │
├─────────────────────────────────────────────────────────────┤
│ Eventually consistent │
│ 最终一致:系统在一定时间后达到一致 │
│ 分区恢复后:通过冲突解决、数据同步达到一致 │
└─────────────────────────────────────────────────────────────┘
【架构权衡】
BASE 理论告诉我们:在工程实践中,不需要追求完美的 CAP 满足,而是通过系统设计,在可用性和一致性之间找到业务可接受的平衡点。
四、生产避坑
4.1 分区期间的决策陷阱
陷阱一:试图在代码层面"绕过"CAP 限制
有些团队试图通过"重试 + 幂等"在 AP 系统中实现 CP:
// ❌ 错误做法:在 AP 系统中强制实现 CP 效果
@Service
public class OrderService {
public void createOrder(Order order) {
// 多次重试试图保证一致性
for (int i = 0; i < 3; i++) {
try {
// 写入分布式存储
distributedStorage.write(order);
// 检查是否写入成功
if (checkConsistency(order)) {
break;
}
} catch (Exception e) {
log.warn("写入失败,重试 {}/3", i + 1);
}
}
}
}
问题:这种方式并不能真正保证 CP,只是降低了不一致的概率。正确的做法是在架构选型阶段就决定 CP 还是 AP。
陷阱二:混淆"可用性"和"可降级"
❌ 可用性 = 系统必须 100% 可用
✅ 可用性 = 系统能正常响应请求(即使返回降级数据)
AP 系统在分区期间:
- ✅ 返回降级数据(可用)
- ✅ 返回默认值(可用)
- ❌ 拒绝所有请求(不可用,这是 CP 的选择)
4.2 CAP 决策树
你需要强一致性吗?
├─ 是(金融、库存、订单)
│ └─ 选择 CP 系统(ZooKeeper、etcd、HBase)
│
└─ 否(社交Feed、日志、展示数据)
└─ 选择 AP 系统(Cassandra、DynamoDB)
└─ 通过补偿机制处理数据冲突
五、工程代价评估
六、落地 Checklist