两阶段提交是分布式事务的第一次认真尝试。它几乎成功了,直到协调者挂掉的那一刻。然后 CAP 定理告诉我们:你不能同时拥有一切。

前情回顾

上一篇我们看到,分布式系统面临三大困难:

  • 网络不可靠:消息会丢、会重复、会乱序
  • 时钟不一致:不能用时间戳判断先后
  • 节点会挂:部分失效是常态

今天我们来看人类的第一次尝试:两阶段提交(2PC)

从一个婚礼说起

想象一场婚礼。牧师问:

牧师:「张三,你愿意娶李四吗?」张三:「我愿意。」

牧师:「李四,你愿意嫁给张三吗?」李四:「我愿意。」

牧师:「我宣布你们结为夫妻!」

这就是两阶段提交的核心思想:

  • 第一阶段(准备):问所有参与者「你准备好了吗?」
  • 第二阶段(提交):如果都说准备好了,就正式提交

2PC 的工作流程

假设我们要完成一笔跨行转账:北京银行扣 100 元,上海银行加 100 元。

正常流程

协调者                北京银行          上海银行
   │                    │                │
   │ [阶段1: 准备]       │                │
   │                    │                │
   │─── 准备扣100元 ───►│                │
   │─── 准备加100元 ─────────────────────►│
   │                    │                │
   │◄── 准备好了 ────────│                │
   │◄── 准备好了 ───────────────────────│
   │                    │                │
   │ [阶段2: 提交]       │                │
   │                    │                │
   │─── 提交! ─────────►│                │
   │─── 提交! ──────────────────────────►│
   │                    │                │
   │◄── 完成 ────────────│                │
   │◄── 完成 ─────────────────────────────│

如果有人没准备好

协调者                北京银行          上海银行
   │                    │                │
   │─── 准备扣100元 ───►│                │
   │─── 准备加100元 ─────────────────────►│
   │                    │                │
   │◄── 准备好了 ────────│                │
   │◄── 余额不足! ──────────────────────│
   │                    │                │
   │ [收到一个拒绝,全部回滚]              │
   │                    │                │
   │─── 回滚! ─────────►│                │
   │─── 回滚! ──────────────────────────►│

看起来很完美?问题来了。

2PC 的致命缺陷

缺陷一:协调者单点故障

如果协调者在第二阶段开始前挂了:

协调者                北京银行          上海银行
   │                    │                │
   │◄── 准备好了 ────────│                │
   │◄── 准备好了 ───────────────────────│
   │                    │                │
   X (协调者挂了)        │                │
                        │                │
                   我该怎么办?      我该怎么办?
                   提交?回滚?      提交?回滚?

参与者陷入困境:

  • 不能提交:万一另一个参与者要回滚呢?
  • 不能回滚:万一另一个参与者已经提交了呢?
  • 只能等待:资源被锁住,一直等协调者恢复

这就是 2PC 的「阻塞问题」。

缺陷二:资源长时间锁定

在等待期间,参与者必须锁住相关资源:

北京银行
    │  张三账户:已锁定,等待最终决定
    │  这时候张三想查余额?抱歉,等着
    │  这时候有人要给张三转账?抱歉,等着
    ▼  (一直等到协调者恢复或超时)

在高并发场景下,这种阻塞是致命的。

缺陷三:网络分区时的困境

如果网络分区发生在第二阶段:

协调者                北京银行          上海银行
   │                    │                │
   │─── 提交! ─────────►│  ✓ 收到        │
   │─── 提交! ──────────────X (网络断了)  │
   │                    │                │
   │                北京提交了        上海没收到
   │                    │                │
   │                 数据不一致!

北京执行了提交,上海没收到消息。数据暂时不一致,需要等协调者重试或网络恢复。如果协调者也崩溃且日志丢失,则永久不一致

3PC:尝试解决阻塞

为了解决 2PC 的阻塞问题,有人提出了三阶段提交(3PC):

阶段2PC3PC
1准备CanCommit(询问能否提交)
2提交PreCommit(预提交)
3-DoCommit(正式提交)

3PC 的改进

  • 引入超时机制,参与者不再无限等待
  • 多一个阶段,减少不确定状态的窗口

但 3PC 仍然无法解决网络分区问题。 当网络断开时,两边可能做出不同的决定。

CAP 定理:不可能三角

2000 年,加州大学伯克利分校的 Eric Brewer 提出了一个猜想,2002 年被正式证明为定理:

在一个分布式系统中,当网络分区发生时,你只能在一致性和可用性之间二选一。

三个字母的含义

字母英文含义大白话
CConsistency一致性所有节点看到的数据一样
AAvailability可用性每个请求都能得到响应
PPartition Tolerance分区容忍网络断了系统还能运行

注意:CAP 的 C 和 ACID 的 C 是完全不同的概念

缩写含义关注点
CAP 的 C线性一致性(Linearizability)分布式节点间的数据同步
ACID 的 C一致性约束(Consistency)单节点上的数据完整性(如外键约束)

CAP 的 C 是说「所有节点同时看到相同的最新数据」,ACID 的 C 是说「事务执行后数据库仍满足约束条件」。不要混淆。

为什么三选二?

关键洞察:P 不是可选的。

在分布式系统中,网络分区一定会发生。这不是「如果」,而是「什么时候」。

所以真正的选择是:当网络分区发生时,你选 C 还是 A?

一个具体的例子

假设你有两个数据中心,北京和上海,各存一份用户余额:

北京数据中心        上海数据中心
余额 = 100          余额 = 100

现在网络断了。北京用户要充值 50 元。

选择 1:保 C(一致性),牺牲 A(可用性)

北京数据中心
    │  用户:我要充值 50 元
    │  系统:抱歉,我联系不上上海
    │        为了保证数据一致
    │        这笔操作暂时无法完成
    │        请稍后再试
    ▼  用户请求被拒绝

选择 2:保 A(可用性),牺牲 C(一致性)

北京数据中心        上海数据中心
    │                   │
    │  用户:充值50元     │
    │                   │
    │  系统:好的!       │
    │  余额 = 150        │  余额 = 100 (不知道北京发生了什么)
    │                   │
    ▼                   ▼
    两边数据不一致

你必须选一边。这就是 CAP。

CP vs AP:两种架构风格

CP 系统:宁可不可用,也要一致

典型代表:

  • ZooKeeper:分布式协调服务
  • etcd:Kubernetes 的配置存储
  • HBase:强一致的分布式数据库

适用场景:

  • 金融交易
  • 库存扣减
  • 订单创建
  • 任何「不能出错」的场景

代价:

  • 网络分区时,部分请求会失败
  • 延迟较高(需要等多数节点确认)

AP 系统:宁可不一致,也要可用

典型代表:

  • Cassandra:高可用分布式数据库
  • DynamoDB:AWS 的 NoSQL 服务
  • DNS:域名解析系统

适用场景:

  • 社交媒体动态
  • 用户浏览历史
  • 购物车(临时数据)
  • 任何「可以容忍短暂不一致」的场景

代价:

  • 可能读到旧数据
  • 需要处理数据冲突

一张图对比

维度CP 系统AP 系统
网络分区时拒绝部分请求继续服务
数据状态强一致最终一致
延迟较高较低
适用场景金融、库存、订单社交、缓存、日志
典型代表ZooKeeper, etcdCassandra, DynamoDB

CAP 的常见误解

误解一:「三选二」意味着放弃一个

不是的。 CAP 说的是「当 P 发生时」的选择。

在网络正常时,你可以同时拥有 C 和 A。只有网络分区发生的那一刻,你才需要做选择。

误解二:系统必须全局选择 CP 或 AP

不是的。 同一个系统的不同部分可以做不同选择。

例如一个电商系统:

  • 库存服务:CP(不能超卖)
  • 商品评论:AP(晚几秒显示没关系)
  • 用户购物车:AP(临时数据,可以容忍短暂不一致)

误解三:CAP 是唯一需要考虑的

不是的。 还有延迟、吞吐量、数据持久性等其他维度。

后来的 PACELC 定理对此做了扩展:

如果有分区(P),选 A 还是 C?

否则(E = Else),选低延迟(L)还是强一致(C)?

2PC 在 CAP 中的位置

现在我们可以理解 2PC 的问题了:

2PC 试图实现 CP:在网络分区时,它选择阻塞(牺牲可用性)来保证一致性。

但它做得不够好:

  • 协调者单点故障导致「既不 C 也不 A」
  • 网络分区时可能导致数据不一致
  • 长时间锁定资源

2PC 是一个不完美的 CP 实现。

那怎么办?

2PC 的核心问题是:它依赖单个协调者来做决定

如果我们不依赖单点呢?如果我们让多个节点一起投票决定呢?

这就是下一篇的主题:Paxos 和 Raft——让一群节点达成共识的算法。

常见问题

Q:既然 2PC 有这么多问题,为什么银行还在用?

A:因为场景匹配 + 补偿机制。

  1. 银行内部网络相对可靠,分区概率低
  2. 业务量相对可控,阻塞影响有限
  3. 配合超时回滚 + 人工对账,能处理异常情况
  4. 强一致性在金融场景是刚需,宁可慢也不能错

Q:CAP 是不是说分布式系统一定有缺陷?

A:是的,但这是物理定律决定的。

光速有限,网络延迟不可避免。当两个节点之间的通信需要时间,你就必须在「等待确认」和「先响应」之间做选择。

这不是算法的问题,是物理世界的限制。承认这个限制,才能设计出正确的系统。

Q:有没有办法绕过 CAP?

A:不能绕过,但可以优化权衡。

  • 减少分区影响范围:多机房部署、网络冗余
  • 缩短不一致窗口:更快的同步机制
  • 根据业务选择:核心数据 CP,非核心数据 AP
  • 混合策略:平时 AP,关键操作 CP

总结

2PC 的贡献与局限

贡献局限
提出了分布式事务的基本框架协调者单点故障
「准备-提交」两阶段思想网络分区时可能不一致
成为后续算法的基础资源长时间锁定

CAP 定理告诉我们

在分布式系统中,当网络分区发生时,你必须在一致性和可用性之间做选择。

没有银弹,只有权衡。

这不是悲观的结论,而是设计分布式系统的基础认知。知道了边界在哪里,才能在边界内做出最优选择。


2PC 的核心问题是依赖单个协调者。如果协调者挂了,系统就瘫痪了。

有没有办法让一群节点自己选出一个领导者?有没有办法在领导者挂了之后自动选出新的?

下一篇,我们来看 Paxos 和 Raft——两个改变分布式系统历史的共识算法。


上一篇:单机到分布式:一致性为何变难

下一篇:Paxos 与 Raft:让多数人达成共识

本系列:

  1. 单机到分布式:一致性为何变难
  2. 2PC 与 CAP:理想的破灭(本篇)
  3. Paxos 与 Raft:让多数人达成共识
  4. 最终一致性:不强求,但终会一致
  5. CRDT:无需协调的合并魔法
  6. 现代方案:从 Spanner 到 TiDB
  7. 实战篇:方案选型与落地