一次提交跨过多个服务后:2PC、3PC、CAP 与 TCC 的取舍
单机事务把提交和回滚封装在一个数据库内部。操作一旦跨过数据库或服务边界,情况就变了:一个节点已经执行成功,另一个节点没有响应,协调者既不能确认对方失败,也不能确认成功消息只是暂时没有返回。此时要解决的不只是如何提交,还包括故障期间要不要等待、恢复后如何续接,以及重复消息会不会把同一笔业务执行两次。
2PC、3PC 和 TCC 都试图组织一次跨节点提交,CAP、异步复制与 Quorum 则解释了网络分区出现后为什么总要舍弃一些东西。把这些内容放在一起看,可以更清楚地分辨:哪些方案在解决原子提交,哪些方案在调整一致性与可用性的边界。
一次操作跨过两个节点,问题就不再只是回滚
假设一笔操作需要同时修改 A、B 两个节点。A 已经完成本地事务,B 却迟迟没有返回。A 看到的只是“没有收到消息”:B 可能宕机了,可能仍在执行,也可能已经成功,只是响应丢失。如果 A 直接回滚,B 可能已经提交;如果 A 继续提交,B 又可能根本没有完成。
网络中的超时只能说明等待超过了某个期限,无法证明远端没有执行。分布式事务协议因此需要同时处理两件事:所有参与者最终应当采用同一个结果,以及在结果暂时无法确定时,每个节点应该停在哪个可恢复的状态。
这也是分布式事务比本地事务麻烦的地方。原子性不再由一个数据库进程独立保证,而要建立在多次消息交换、持久化状态和故障恢复之上。
2PC 用两轮通信换取统一决定
两阶段提交(Two-Phase Commit,2PC)引入一个协调者和多个参与者。协调者不直接假定大家都能提交,而是先收集投票,再公布全局结果。
第一阶段通常称为 Prepare。协调者询问所有参与者能否提交。参与者完成必要的本地检查,写入可恢复的事务状态,并保留提交所需的资源;如果返回同意,就表示自己已经具备之后提交的条件,不能再随意单方面退出。
1 | 协调者 ── Prepare ──→ 参与者 A、B、C |
第二阶段由协调者汇总投票。只有所有参与者都同意,协调者才记录 Commit 决定并通知大家提交;只要有一个参与者拒绝或准备失败,就通知所有参与者 Abort。参与者完成第二阶段后释放第一阶段保留的资源。
1 | 全部同意:协调者 ── Commit ──→ A、B、C |
在没有故障的正常路径上,从协调者与参与者之间的交互看,Prepare 及其响应是一轮,最终决定及确认又是一轮,通常可以概括为两次往返通信。代价不只在网络时延:参与者进入 Prepared 状态后,还可能继续持有锁或其他资源,直到全局结果确定。
协调者宕机后,为什么参与者不能自己回滚
2PC 最棘手的状态是:参与者已经对 Prepare 投了同意票,却还没有收到第二阶段决定。此时协调者如果失联,参与者不能仅凭超时回滚,因为协调者可能已经决定提交,并把 Commit 发给了其他参与者;它也不能自行提交,因为仍可能有人在第一阶段投了反对票。
给协调者增加一个 watchdog 或备用节点可以改善恢复能力,但备用节点不能只靠“重新问一遍”就安全接管。它至少需要访问协调者的持久化事务日志,确认全局决定是否已经产生;还需要可靠的主节点切换和隔离机制,避免新旧协调者同时工作。参与者同样要保存 Prepared、Committed 或 Aborted 等可恢复状态,重启后才能继续未完成的事务。
如果原协调者的决定无法恢复,同时又有参与者宕机或网络不可达,新协调者便可能缺少作出安全决定所需的信息。其他参与者只能继续等待。这就是 2PC 的阻塞性:日志和协调者高可用能缩短很多故障的恢复时间,却没有改变协议在信息不足时必须等待的边界。
3PC 增加一个阶段,换来的是有条件的非阻塞
三阶段提交(Three-Phase Commit,3PC)在“询问能否提交”和“执行最终提交”之间增加了 PreCommit 阶段。协调者先收集所有参与者的同意票,再通知大家进入预提交状态;参与者确认收到 PreCommit 后,协调者才发送最终 Commit。
1 | CanCommit? → PreCommit → DoCommit |
多出的状态让参与者知道更多信息。未进入 PreCommit 的参与者在超时后可以倾向于中止;已经进入 PreCommit 的参与者则知道所有人此前都同意了提交。在崩溃故障、通信延迟有上界等假设下,节点可以通过状态和超时规则继续推进,减少 2PC 中长期等待协调者的情况。
不过,3PC 不能在任意网络分区下同时保证原子性和非阻塞。两个彼此失联的分区可能基于各自看到的状态作出不同处理;如果为了避免分歧而等待,阻塞又会回来。它还比 2PC 多一轮通信。3PC 更适合用来理解“增加一层已知状态为何能缓解阻塞”,不能简单理解为给 2PC 加一个阶段后就彻底解决了故障问题。
参与者依据日志恢复也不是 3PC 独有的能力。2PC 的 XA 资源同样需要持久化 Prepared 分支并支持恢复扫描。3PC 真正增加的是一个协议状态以及与之配套的超时决策条件。
CAP 只在发生分区时迫使系统做选择
CAP 中的三个概念有比较严格的含义:
- 一致性(Consistency)指线性一致性。一次成功写入之后,后续操作表现得像所有请求都在一个单一、实时有序的数据副本上执行。
- 可用性(Availability)指每个到达未故障节点的请求最终都能得到响应。它讨论的是请求能否结束,并不等同于日常监控里的固定延迟 SLA。
- 分区容错性(Partition Tolerance)指节点之间出现任意消息丢失或网络隔离时,系统仍要按既定规则运行。
CAP 不是让系统平时从三个字母里任选两个。没有网络分区时,一致性和可用性可以同时满足;分区真正发生后,无法通信的两侧才必须在 C 与 A 之间取舍。坚持线性一致性的一侧需要拒绝或等待一部分无法确认的请求,牺牲可用性;允许每个分区继续接受请求,则可能产生彼此不可见的版本,暂时失去线性一致性。
工程上通常不能把 P 当成一个可以关闭的功能。网络和节点故障无法被永久排除,而且从观察者的角度看,一个远端节点宕机与发往它的消息持续丢失可能表现得完全一样。两者在概念上并不相同,却都会制造“无法确认对方状态”的局面。
线性一致性也不是唯一的一致性模型。序列一致性要求所有操作能排成一个全局顺序,并保持每个客户端自己的程序顺序,但不要求这个顺序符合真实时间;最终一致性则允许副本暂时不同,只要求在没有新更新且通信恢复后逐渐收敛。放宽哪一部分约束,决定了系统还能保留多少可用性与性能空间。
异步复制和 Quorum 没有绕过 CAP
异步复制常见的路径是:主节点先完成本地写入并向调用方返回,再把变更传播到其他副本。它缩短了写请求等待副本的时间,但复制尚未完成时,从落后副本读取就可能得到旧数据。因此,异步复制本身提供不了强一致读;若允许多个隔离节点同时接受写入,还必须处理版本冲突。
Quorum 用 N、W、R 描述副本访问条件:N 是副本总数,W 是一次成功写入需要确认的副本数,R 是一次读取需要返回的副本数。当 R + W > N 时,读集合与最近一次成功写集合至少有一个交点;当还需要避免两个写集合完全分离时,通常要求 W > N / 2。
集合相交只是读到新版本的重要基础,不能单独推出完整的线性一致性。系统还需要版本比较、并发写入排序、故障切换和冲突处理等机制。分区发生后,如果某一侧凑不齐要求的 W 或 R,它就必须拒绝请求;如果降低门槛让两侧都继续写,系统则要接受暂时不一致并在之后合并。
以高可用为目标的 Dynamo 就明确选择了在部分故障场景中牺牲一致性,并借助对象版本和冲突解决让副本之后收敛。它说明的不是“Quorum 同时拿到了 CAP 三项”,而是 N、R、W 可以把一致性、延迟和可用性的取舍做成可配置策略。
收不到确认时,重试为什么还不够
分布式事务中的不确定性与“两军问题”有相似之处:在不可靠通信中,消息发送方无法仅靠有限次确认获得双方都知道结果的绝对把握。一次超时之后重发消息,可以提高最终送达的概率,却会引入另一个问题——上一条消息也许已经被执行,只是响应丢了。
所以重试必须和幂等一起设计。每个事务或分支需要稳定的唯一标识,参与者要记录自己是否处理过该阶段;同一个 Commit、Cancel 或业务请求再次到达时,应返回已有结果,而不是再次扣减库存、划转资金或释放资源。协调者也要持久化状态,重启后从原阶段继续,而不是新建一笔语义相同的事务。
重试也不等于一定成功。永久故障、业务校验失败和无法自动消解的冲突仍需要超时终止、告警、对账或人工处理。幂等解决的是“重复执行不能产生额外副作用”,持久化与恢复解决的是“进程重启后还能继续”,二者共同缩小不确定状态的影响。
TCC 把两阶段提交搬到业务接口上
传统 2PC 常通过 XA 协议协调数据库、消息系统等资源管理器。事务管理器调用各资源的 Prepare、Commit 和 Rollback 接口,适合参与资源都实现 XA 契约的场景。微服务之间更多通过 RPC 组合业务能力,协调者无法直接控制每个服务内部的数据库事务,TCC 因而把准备、提交和回滚改成显式业务接口。
TCC 是 Try、Confirm、Cancel 的缩写:
- Try 检查业务条件并预留资源,例如冻结额度或预占库存,但不完成最终消费。
- Confirm 在所有 Try 都成功后确认使用这些资源。
- Cancel 在任一 Try 失败或全局事务中止时释放预留资源,恢复到可继续处理的业务状态。
1 | Try(A) ─┐ |
从协调流程看,TCC 是一种应用层两阶段协议。它不要求底层资源支持 XA,资源预留粒度也可以按业务设计,代价是事务语义进入了服务代码。每个参与服务都要实现三套行为,并处理 Confirm、Cancel 的幂等、重试与恢复。
实际实现还要防止两个常见时序问题。第一种是空回滚:协调者没有确认 Try 成功,却因为超时发送了 Cancel,服务需要安全地记录“该分支已取消”,不能把不存在的预留当成异常副作用。第二种是悬挂:Cancel 已经执行后,迟到的 Try 又到达;服务必须识别该事务已经结束,拒绝再次预留资源。
TCC 的第二阶段通常由协调者持续重试,但“持续重试”仍要有运维出口。无法自动完成的分支需要进入告警、对账或人工修复流程。TCC 能把跨服务事务变成可补偿、可恢复的业务过程,却不会自动提供数据库事务那样的隔离性,Try 阶段预留了什么、其他请求能看到什么,都要在业务模型中明确。
这些模型解决的不是同一个层面
| 方案 | 主要处理对象 | 主要优势 | 需要接受的代价或边界 |
|---|---|---|---|
| XA / 2PC | 支持事务协议的数据库、消息系统等资源 | 由事务管理器统一决定提交或回滚 | Prepared 状态可能持有资源;协调信息不足时会阻塞 |
| 3PC | 带特定故障与时序假设的原子提交 | 通过 PreCommit 降低部分崩溃场景中的阻塞 | 多一轮通信;网络分区下仍无法兼得安全与非阻塞 |
| TCC | 跨服务的业务资源 | 不依赖底层 XA,可自行控制资源预留粒度 | 业务侵入高,必须处理幂等、空回滚、悬挂和人工恢复 |
| 异步复制 / Quorum | 同一份数据的多副本读写 | 可以调节一致性、延迟与可用性 | 不是跨资源原子提交;保证取决于读写门槛和冲突处理 |
选择方案时,先要确认问题究竟是“多个资源必须原子提交”,还是“同一份数据需要跨副本容错”。前者关注全局事务决定,后者关注副本一致性与分区期间的服务策略。把两者混在一起,很容易把最终一致复制误当成事务提交,或者期待 2PC 解决网络分区下的可用性。
分布式事务最终都要面对同一个事实:节点只能根据本地状态和已经收到的消息行动。协议能做的是记录更多可恢复状态、规定超时后的合法动作,并把无法自动判断的情况暴露给重试、对账与人工处理。具体系统采用哪一种模型,应当从参与者能力、可接受的不一致窗口、资源占用时间和故障恢复方式开始判断。