1978年,图灵奖得主Jim Gray在《Notes on Data Base Operating Systems》中正式描述了两阶段提交协议(Two-Phase Commit, 2PC)。这个协议的核心目标简单而直接:让分布在不同机器上的多个数据库,能够像操作单一数据库一样实现"全有或全无"的原子性保证。

四十七年后的今天,分布式事务依然是分布式系统中最棘手的问题之一。从银行转账到电商订单,从机票预订到支付结算,任何涉及多个服务或数据库的操作都绕不开这个问题。而架构师们面临的两难选择从未改变:追求强一致性会牺牲可用性和性能,追求高可用又会面临数据不一致的风险。

阻塞的代价:两阶段提交的根本缺陷

两阶段提交的工作原理直观明了。第一阶段,协调者向所有参与者发送"准备"请求,参与者执行事务操作但不提交,写入redo/undo日志后回复"就绪"。第二阶段,如果所有参与者都回复就绪,协调者发送"提交"指令;否则发送"中止"指令。

问题出在协调者崩溃的时刻。

当所有参与者都回复"就绪"后,它们进入一个被称为"不确定状态"(uncertain state)的阶段。此时,任何一个参与者都无法单方面决定提交还是中止——它不知道其他参与者是否也回复了就绪,也不知道协调者最终的决定是什么。唯一的选择是等待协调者恢复。

Jim Gray和Leslie Lamport在2004年的论文《Consensus on Transaction Commit》中给出了精确的定义:“两阶段提交协议在协调者故障时会阻塞,没有进程能知道事务的最终结果,直到协调者被修复。”

这种阻塞不是理论上的可能性,而是实际生产环境中反复发生的噩梦。假设一个银行系统使用2PC处理跨行转账,涉及A银行、B银行和中央清算系统三个参与者。当所有银行都回复"就绪"后,清算系统的协调者服务器遭遇硬件故障。此时,两家银行的资金都被锁定——存款被冻结,但转账尚未完成。客户无法取款,银行间清算停滞,直到协调者服务器恢复。如果恢复需要数小时,整个业务链条都会陷入瘫痪。

阻塞问题的根源在于协议设计本身。2PC本质上是一个"全或无"的决策过程,协调者拥有单点决策权。当这个决策点失效时,整个系统陷入僵局。

FLP不可能性定理的阴影

1985年,Fischer、Lynch和Patterson发表了著名的FLP不可能性定理:在异步分布式系统中,即使只有一个进程故障,也不存在确定性的共识算法能够保证有限时间内达成一致。

分布式事务的提交决策本质上是一个共识问题——所有参与者需要对"提交"还是"中止"达成一致。FLP定理意味着:在纯粹的异步模型下,没有任何协议能够同时保证安全性(不会出现部分提交部分中止)和活性(最终一定会完成)。

两阶段提交选择牺牲活性来换取安全性。当协调者故障时,参与者选择阻塞而不是冒险做出可能与其他参与者不一致的决定。这是一个深思熟虑的权衡,但代价高昂。

从三阶段提交到Paxos Commit:非阻塞协议的探索

1983年,Skeen和Stonebraker提出了三阶段提交协议(3PC),试图解决2PC的阻塞问题。3PC引入了两个关键改进:

超时机制:参与者在每个阶段都有超时时间。如果在预定时间内没有收到协调者的响应,参与者可以主动中止事务。

预提交阶段:在最终提交之前增加一个"预提交"阶段。协调者收集所有"就绪"响应后,先发送"预提交"指令,收到确认后再发送"提交"指令。

理论上,这解决了协调者单点故障问题。当协调者在预提交阶段崩溃时,参与者可以通过互相通信确定系统的全局状态:如果有人收到了预提交,说明所有人都回复了就绪,可以安全提交;如果没有人收到预提交,说明投票阶段还没完成,可以安全中止。

但3PC有一个致命缺陷:它假设网络是可靠的。在网络分区(network partition)的情况下,系统可能分裂成两个无法通信的分区。一个分区决定提交,另一个分区决定中止——违反了事务的原子性。这就是为什么3PC在实践中很少被采用。

Paxos Commit:用共识协议解决提交问题

Gray和Lamport在2004年提出了一个更优雅的解决方案:Paxos Commit。核心思想是将提交决策从单一协调者转移到一组接受者(acceptors),使用Paxos共识协议来达成决策。

具体实现中,每个参与者运行一个独立的Paxos实例来决定自己应该"准备"还是"中止"。事务被提交当且仅当所有参与者的Paxos实例都选择了"准备"。使用2F+1个接受者,系统能够容忍F个接受者故障而不阻塞。

Paxos Commit在消息延迟上只比2PC多一个往返(正常情况下是5个消息延迟 vs 2PC的4个),但换来了真正的容错能力。这是分布式事务领域的一个重要里程碑——证明了非阻塞的原子提交协议是可以实现的,只要愿意付出更多的消息成本。

Saga模式:接受不一致,管理不一致

1987年,普林斯顿大学的Hector Garcia-Molina和Kenneth Salem发表了《Sagas》论文,提出了一个根本不同的思路:放弃强一致性,接受中间状态可见,通过补偿机制来保证最终一致性。

Saga的核心思想是将一个长事务分解为一系列有序的本地事务T₁, T₂, …, Tₙ。每个本地事务都是原子的,在单个服务内部执行。如果所有事务都成功,整个Saga完成。如果某个事务Tᵢ失败,则依次执行补偿事务Cᵢ₋₁, Cᵢ₋₂, …, C₁来撤销之前已完成的操作。

补偿事务的设计哲学

补偿事务不是简单的回滚。当T₁执行完成后,它的效果可能已经被其他事务观察到——订单已创建、库存已扣减、消息已发送。补偿事务需要"语义上撤销"这些操作,而不是简单地恢复数据库状态。

考虑一个机票预订Saga:

  • T₁:预订机票(状态:已预订)
  • T₂:扣减里程积分
  • T₃:发送确认邮件

如果T₃失败(邮件服务宕机),补偿事务C₂需要恢复里程积分,C₁需要取消预订。但关键问题是:在T₂执行后、C₁执行前,其他用户查询座位时看到的是什么状态?如果座位显示"已预订",其他用户就无法预订这个座位,即使这个Saga最终会失败。

这就是Saga最大的挑战:缺乏隔离性。在Saga执行过程中,其他事务可能读取到"脏数据"——已经提交但最终可能被补偿的数据。

隔离性缺失的四种异常

Azure架构中心的技术文档详细描述了Saga模式中可能出现的四种数据异常:

丢失更新(Lost Updates):Saga A读取数据后,Saga B修改了同一数据并提交,Saga A随后也修改该数据,覆盖了Saga B的更新。

脏读(Dirty Reads):Saga A执行了T₁,修改了某条记录但尚未完成整个Saga。Saga B读取了这条记录,得到了一个最终可能被撤销的数据。

不可重复读(Non-repeatable Reads):Saga在执行过程中多次读取同一数据,得到不同的结果,因为其他Saga在这期间修改了数据。

幻读(Phantom Reads):Saga第一次查询返回了N条记录,第二次同样的查询返回了不同数量的记录,因为其他Saga插入或删除了匹配的记录。

四种对策:管理可见性的艺术

Chris Richardson在《Microservices Patterns》一书中提出了四种缓解隔离性问题的对策:

语义锁(Semantic Lock):在可补偿事务中设置一个"处理中"的标记,明确告诉其他事务这个数据正在被修改。例如,订单状态设为"PENDING"而不是直接设为"CONFIRMED"。其他事务看到这个状态就知道数据可能变化。

交换更新(Commutative Updates):设计更新操作使得执行顺序不影响最终结果。例如,账户余额的加减操作是可交换的——先加100再减50,与先减50再加100,结果相同。这种设计天然避免了丢失更新问题。

悲观视图(Pessimistic View):重新排列Saga的步骤顺序,将可能产生脏读风险的更新操作放在不可补偿的事务之后。一旦执行了不可补偿事务,Saga就必须完成,因此后续步骤不会产生脏数据。

重读值(Reread Value):在执行更新前重新读取数据,检查是否发生了变化。如果数据已被其他事务修改,则中止当前操作并重新开始Saga。

这四种对策不是互斥的,在实践中往往组合使用。但它们都指向一个事实:Saga把隔离性的责任从数据库层面转移到了应用层面。开发者需要理解业务语义,判断哪些异常是可接受的,哪些必须预防。

TCC模式:显式的两阶段补偿

Try-Confirm-Cancel(TCC)模式是Saga思想的一种具体实现,由Atomikos公司在2006年提出,专门用于微服务架构中的分布式事务。

TCC将每个操作明确分为三个阶段:

Try:预留资源,但不真正执行。例如,转账场景中,Try阶段冻结转出账户的金额,但不实际扣款。

Confirm:确认执行,真正完成操作。释放预留的资源,执行实际业务。

Cancel:取消预留,回滚Try阶段的操作。解冻金额,释放资源。

sequenceDiagram
    participant Client
    participant Coordinator as 协调者
    participant ServiceA as 服务A
    participant ServiceB as 服务B
    
    Client->>Coordinator: 发起事务
    Coordinator->>ServiceA: Try (预留资源)
    ServiceA-->>Coordinator: 成功
    Coordinator->>ServiceB: Try (预留资源)
    ServiceB-->>Coordinator: 成功
    Coordinator->>ServiceA: Confirm (确认执行)
    Coordinator->>ServiceB: Confirm (确认执行)
    Coordinator-->>Client: 事务完成

TCC与2PC的关键区别在于:2PC的"准备"阶段由数据库实现,锁定的是数据库行锁;TCC的"Try"阶段由业务代码实现,锁定的是业务语义层面的资源。这带来了两个优势:

首先,锁粒度更细。2PC在整个准备阶段持有数据库锁,其他事务无法访问这些数据。TCC的Try阶段只是业务层面的"预留",数据库层面的数据仍然可读可写。

其次,更适合异构系统。当事务涉及数据库、消息队列、缓存、第三方API等不同类型的资源时,2PC的XA协议难以统一管理,而TCC可以为每种资源定义自己的Try/Confirm/Cancel逻辑。

但TCC也有显著缺点:开发成本高。每个操作都需要实现三个方法,且必须保证Confirm和Cancel的幂等性——因为网络超时可能导致重复调用。更重要的是,Cancel操作必须是真正可补偿的,这在某些场景下极其困难。

现代分布式数据库的事务实现

当分布式事务遇上分布式数据库,问题的复杂性再次提升。CockroachDB、TiDB、Google Spanner等新一代分布式SQL数据库展示了如何在全球化部署下实现ACID事务。

Spanner的TrueTime:用物理时钟打破不可能

2012年,Google发表了Spanner论文,引入了TrueTime API——一个提供有界误差时钟的服务。TrueTime返回的不是单一时间戳,而是一个时间区间[earliest, latest],保证真实时间一定落在这个区间内。

这个设计打破了传统分布式系统中对时钟不可靠的假设。通过使用GPS和原子钟的双重冗余,Spanner将时钟误差控制在毫秒级别。更重要的是,Spanner在提交事务时会等待超过时钟误差的时间,确保所有副本都对事务顺序达成一致。

这种"等待时间"的设计违反了人们对高性能系统的直觉,但在全球化部署中,几毫秒的等待换取了严格的外部一致性(External Consistency)——一个比线性一致性更强的保证。

CockroachDB的Parallel Commits:优化两阶段提交

CockroachDB在2019年推出了Parallel Commits协议,将分布式事务的延迟从两轮共识减少到一轮。

传统两阶段提交在CockroachDB中的实现是:先并行写入所有数据范围(Range),每个范围的写入都需要经过Raft共识;然后等待所有写入完成,再写入事务记录标记"已提交"。这导致了顺序依赖:事务记录的提交必须等待所有数据写入完成。

Parallel Commits的核心创新是引入了一个中间状态STAGING。事务记录不再直接标记"已提交",而是标记为STAGING并记录所有待写入的键列表。当观察者看到一个STAGING状态的事务时,它需要验证所有写入是否已成功复制——如果都成功了,事务就是已提交的;如果有任何写入失败,事务就是已中止的。

关键点在于:写入事务记录和写入数据可以并行进行。协调者发起所有写入(包括事务记录和数据),然后等待它们全部完成。从观察者的角度看,只要所有写入都成功了,事务在任何时刻都是原子的。

Percolator模型:TiDB的选择

TiDB采用Google Percolator模型实现分布式事务。Percolator是为大规模增量处理设计的,其核心是使用多版本时间戳排序(MVCC)实现快照隔离。

每个数据项存储三个列:

  • Data列:存储实际数据值,带时间戳
  • Lock列:存储事务锁信息
  • Write列:存储提交记录,指向Data列的时间戳

事务分为两个阶段:

预写阶段(Prewrite):选择一个主键,对所有涉及的键加锁并写入数据。此时数据还不可见。

提交阶段(Commit):先提交主键(写入Write列并清除Lock列),然后异步提交其他键。

Percolator模型的优势在于读操作不需要加锁——通过读取Write列找到可见版本即可。但代价是写操作需要两轮往返,且锁信息需要额外的存储空间。

XA协议:工业标准的两难

X/Open XA协议是分布式事务处理的工业标准,定义了事务管理器(Transaction Manager)和资源管理器(Resource Manager)之间的接口。大多数主流数据库(MySQL、PostgreSQL、Oracle、SQL Server)都实现了XA接口。

XA协议本质上是对2PC的标准化封装。它定义了xa_start、xa_end、xa_prepare、xa_commit、xa_rollback等API,允许一个全局事务协调多个数据库的本地事务。

但XA在生产环境中的问题相当多:

性能开销:XA事务需要额外的网络往返、日志写入和锁持有时间。在高并发场景下,这些开销会显著影响吞吐量。

死锁风险:当多个XA事务涉及相同的资源时,可能产生跨数据库的死锁。数据库层面的死锁检测只能发现本地死锁,无法处理跨库死锁。

悬挂事务:如果协调者在prepare后、commit前崩溃,参与者会持有锁并等待。即使协调者恢复,也可能因为日志丢失而无法知道事务状态。

MySQL的XA实现尤其脆弱。在主从切换时,prepare状态的XA事务可能丢失;在崩溃恢复时,可能产生悬挂事务。这些问题导致很多团队在生产环境中避免使用XA。

选择的艺术:权衡的博弈

分布式事务领域没有万能药。每种方案都有其适用场景和代价:

强一致性场景(金融核心系统、库存管理):考虑使用支持ACID的分布式数据库(Spanner、CockroachDB、TiDB)。它们在数据库层面解决了分布式事务问题,应用代码相对简单。代价是更高的硬件成本(Spanner需要原子钟)或更复杂的运维。

最终一致性可接受场景(电商订单、内容审核):Saga模式是主流选择。需要仔细设计补偿逻辑,处理隔离性问题,但换取了更好的可用性和性能。建议使用成熟的Saga框架(如Temporal、Seata Saga),而非从零实现。

资源预留场景(票务预订、库存预占):TCC模式适合这类场景。Try阶段预留资源,Confirm确认消费,Cancel释放预留。开发成本较高,但能更好地控制资源使用。

混合场景:Outbox模式是处理数据库写入和消息发送原子性的经典方案。将消息写入本地数据库的Outbox表,与业务数据在同一个本地事务中提交,然后由后台任务扫描Outbox并发送消息。这避免了分布式事务,但引入了最终一致性。

Seata框架的启示

阿里巴巴开源的Seata框架提供了四种事务模式:AT、TCC、Saga、XA。

AT模式是Seata的创新:应用无需编写补偿代码,框架自动生成。原理是在执行SQL前后记录数据快照(前镜像和后镜像),回滚时用前镜像覆盖当前数据。代价是额外的存储开销和潜在的性能影响。AT模式适合对一致性要求不高、希望快速接入的场景。

Seata的经验表明:分布式事务解决方案的选择,本质上是在一致性、可用性、开发效率和性能之间做权衡。没有通用的最优解,只有特定场景下的最优解。

结语

从Jim Gray描述两阶段提交,到Garcia-Molina提出Saga模式,已经过去了近四十年。分布式事务的基本权衡没有改变:强一致性需要付出阻塞的代价,非阻塞协议需要接受更复杂的实现,补偿模式需要开发者承担隔离性的责任。

但技术演进从未停止。Paxos Commit证明了非阻塞原子提交是可行的;TrueTime展示了物理时钟如何在分布式系统中发挥作用;Parallel Commits优化了两阶段提交的延迟;各种框架和模式降低了实现复杂度。

分布式事务不是一个可以"解决"的问题,而是一个需要持续权衡的架构决策。理解每种方案的代价,根据业务场景做出选择,才是架构师真正的挑战。


参考文献

  1. Gray, J. (1978). Notes on Data Base Operating Systems. Operating Systems: An Advanced Course.
  2. Garcia-Molina, H., & Salem, K. (1987). Sagas. Proceedings of the ACM SIGMOD Conference.
  3. Gray, J., & Lamport, L. (2004). Consensus on Transaction Commit. MSR-TR-2003-96.
  4. Fischer, M. J., Lynch, N. A., & Paterson, M. S. (1985). Impossibility of Distributed Consensus with One Faulty Process. Journal of the ACM.
  5. Corbett, J. C., et al. (2012). Spanner: Google’s Globally-Distributed Database. OSDI ‘12.
  6. Peng, D., & Dabek, F. (2010). Large-scale Incremental Processing Using Distributed Transactions and Notifications. OSDI ‘10.
  7. Thomson, A., et al. (2012). Calvin: Fast Distributed Transactions for Partitioned Database Systems. SIGMOD ‘12.
  8. X/Open Company Ltd. (1991). Distributed Transaction Processing: The XA Specification.
  9. Richardson, C. (2018). Microservices Patterns. Manning Publications.
  10. Cockroach Labs. (2019). Parallel Commits: An atomic commit protocol for globally distributed transactions. CockroachDB Blog.