当我们说起事务,表达的是一组不可分割的操作,以及这些操作带来的影响。这些操作要么一起成功,要么一起失败,以此来维护数据的一致性。
无论是什么类型的系统,业务的准确性往往取决于数据的准确性。数据的准确性从读写两方面进行保障。以事务为数据读写的基本单位,写指事务的执行机制,读指事务的隔离等级。读与写互相影响,事务的执行机制决定事务的隔离等级,事务的隔离等级约束事务的执行机制。
分布式事务跨越多个节点,本质上是一个协作问题。本地事务运行也赖于多个成员配合,二者有什么关联与不同,要从数据库本地事务的基本模型说起。
本地事务的基本模型

数据库本地事务是一个log + lock
模型,保障事务的ACID
属性。
- 原子性(Atomicity):原子性要求事务中的所有操作要么全部成功,要么全部失败,数据库通常使用
undo log
来保障原子性。在事务实际修改数据前,数据库会将每个修改操作的逆操作记录到undo log
中,如果事务执行过程中出现错误需要回滚,数据库会按照undo log
中的记录,反向执行这些逆操作,将数据库状态恢复到事务开始之前。 - 一致性(Consistency):一致性确保事务在执行前后,数据库的状态始终满足所有的完整性约束,一致性的保障依赖于原子性、隔离性和持久性。事务的一致性属性在概念上与分布式的数据一致性有所不同,分布式事务的一致性包含了数据一致性和事务原子性的要求。
- 隔离性(Isolation):隔离性要求不同事务之间相互隔离,一个事务的执行不会受到其他事务的干扰。数据库通过锁机制和多版本并发控制(
MVCC
)来实现不同的隔离级别。
- 读未提交(Read Uncommitted):允许事务读取尚未提交的数据更改,是最低的隔离级别,可能会出现脏读现象,即读取到其他事务回滚前的无效数据。
- 读已提交(Read Committed):只能读取到已经提交的数据,避*了脏读,但可能会出现不可重复读问题,即在同一事务中多次读取同一数据时,由于其他事务的修改,导致每次读取的结果不一致。
- 可重复读(Repeatable Read):在一个事务内,多次读取同一数据的结果是一致的,解决了不可重复读问题。通过在事务执行期间对数据进行加锁实现,但可能会出现幻读现象,即当一个事务按照某个条件查询数据时,另一个事务插入了符合该条件的新数据,导致第一个事务再次按照相同条件查询时,得到了不同的结果。可以通过锁粒度策略(如锁膨胀和范围锁)进行一定程度的优化。
- 串行化(Serializable):将所有事务按照顺序依次执行,完全避*了并发问题,是最高的隔离级别,但性能较差。
- 持久性(Durability):持久性保证事务一旦提交,对数据库的修改将永久保存,即使发生系统崩溃或其他故障也不会丢失,数据库通常使用
redo log
来保障持久性。数据库会将对数据的修改先记录到redo log
,然后再逐步应用到实际的数据文件。只要确保redo log
中的记录在事务提交时已被持久化,数据库在故障重启后即能通过重新执行redo log
中记录的操作,将未完成的事务提交,以此保证已提交事务的修改不会丢失。
分布式事务的不同模式
从集中式走向分布式,很多问题的复杂性是由状态不可知引起的,而状态不可知往往又是因为网络的缘故。由于网络天然具备的不确定性,导致异常状态的难以归因,就像久未等来的战报,无法判断是信使的意外还是变节,又或者是城堡早已沦陷。
反过来讲,不同于本地系统获取状态的即时性,分布式场景当下的状态不可知也就代表着获取确定状态需要的时间成本,这个成本可能是信息交换导致的(如网络传输延迟),也可能是节点状态导致的(如等待故障恢复)。
相同步调的协作需要确定的状态来驱动,分布式系统获取状态的时间成本已无法忽略,必然导致事务的ACID
属性与系统的可用性、扩展性和性能之间的权衡取舍,从而允许分布式事务的设计从ACID
硬约束向BASE
原则柔性转变。
- 基本可用(Basically Available):在分布式系统出现故障时,允许系统的某些部分仍然可用,保证核心功能的基本运行,而不是让整个系统完全崩溃。这个原则的具体表现之一是允许一定程度的分区自治。
- 软状态(Soft State):允许系统中的数据在一段时间内处于中间状态,即数据可以暂时不满足强一致性要求。这种中间状态是由于数据在分布式系统中的复制、传播和更新存在延迟所导致的。
- 最终一致性(Eventual Consistency):尽管系统中的数据在一段时间内可能处于不一致状态,但经过一段时间的处理和同步,最终所有数据将达到一致状态。一般通过超时处理和补偿机制来保障。
从大的方面上看,数据一致性模型是分布式事务不同模式的主要考虑因素,其中,2PC和3PC是以数据强一致性为设计目标的事务模式,TCC稍显折中,Sagas是以数据最终一致性为设计目标的事务模式。
2PC
二阶段提交(Two-Phase Commit,2PC)将分布式事务的提交过程分为两个阶段:准备阶段和提交阶段。通过引入一个协调者(Coordinator
)来协调各个参与者(Participants
)的操作,确保所有参与者要么都提交事务,要么都回滚事务,从而保证分布式事务的一致性。

二阶段提交中每个参与者的本地事务模型与前面提到的基本模型一致,这也意味着2PC的适用面有较强的数据库语义,一般用于协调分布式系统中多个数据库实例完成事务性操作。
可以将二阶段提交理解为一个公共Coordinator
协调了一组Participants
,执行了一批关闭了autocommit
的数据库事务,基本过程如下:
- 准备阶段(投票阶段)
- 协调者向所有参与者发送
Prepare
消息,询问它们是否可以提交事务。 - 参与者收到消息后,执行事务操作,但不提交。记录事务日志,包括
redo log
和undo log
。 - 参与者根据自身执行情况向协调者反馈
Yes
或No
。
- 提交阶段(决策阶段)
- 如果协调者收到所有参与者的反馈都是
Yes
,那么它会向所有参与者发送Commit
消息,通知它们提交事务。参与者收到Commit
消息后,正式提交事务,并将事务状态持久化到存储中。 - 如果协调者收到任何一个参与者返回的是
No
,或者在规定时间内没有收到所有参与者的响应,那么它会向所有参与者发送Abort
消息,通知它们回滚事务。参与者收到Abort
消息后,根据之前记录的日志进行回滚操作,将数据恢复到事务开始前的状态。
二阶段提交的优缺点都很明显。优点是实现简单,能保证分布式环境下事务的一致性。缺点体现在几方面:
- 单点故障:协调者故障会导致分布式事务无法推进,局部数据也可能出现不一致的情况。可以增加选举机制集群化协调者来应对这个问题,但会引入额外复杂度。
- 性能问题:协调者和参与者需要进行多轮全量消息交互和等待确认,由于木桶的短板效应,事务受限于最慢的网络延迟和最长的处理开销,事务整体处理性能较低。
- 阻塞问题:参与者执行完事务操作后会锁定相关资源,直到收到协调者的最终决策消息。如果在这个过程中出现网络隔离或其他问题,会导致参与者长时间等待,造成资源的长时间阻塞,进一步恶化系统性能。
二阶段提交适合两类场景:一类是对一致性要求极高,甚至高到可以忽略其他质量属性影响的场景,如银行转账和金融交易领域;另一类是参与者较少的小规模分布式系统,二阶段提交的性能开销相对可以接受。
3PC
三阶段提交(Three-Phase Commit,3PC)是在二阶段提交基础上进行改进的一种分布式事务提交协议,它将事务的提交过程分为三个阶段,以减少二阶段提交中存在的阻塞问题,提高系统的可用性和容错性。
二阶段提交的阻塞来源从协调者的角度是其自身的可用性,从参与者的角度看是指令下达的时效性。此外,准备阶段过重的事务操作导致2PC的中止操作同样很重,对并发事务竞争非常不友好,频繁的锁定-执行-记录-回滚
既浪费了无谓的执行开销,也降低了全局事务的吞吐量。

三阶段提交通过更轻量的问询阶段和赋予参与者一定自治能力来优化二阶段提交中的阻塞问题,基本过程如下:
- CanCommit阶段
- 协调者向所有参与者发送
CanCommit
请求,询问它们是否可以执行事务提交操作。 - 参与者收到请求后,检查自身资源是否能够满足事务的要求,如是否能获取锁、是否能执行SQL等。
- 参与者根据自身检查情况向协调者反馈
Yes
或No
。
- PreCommit阶段
- 如果协调者收到所有参与者返回的
Yes
响应,那么进入PreCommit
阶段。协调者向所有参与者发送PreCommit
请求,通知它们准备提交事务。 - 参与者收到
PreCommit
请求后,开始执行事务操作,记录事务日志,但不提交事务。这与二阶段提交的准备阶段类似,不过在三阶段提交中,参与者可以不阻塞等待协调者的最终决策,而是继续执行其他非事务操作,只是在收到最终决策前不能提交该事务。 - 如果协调者收到任何一个参与者返回的
No
响应,或者在规定时间内没有收到所有参与者的响应,那么它会向所有参与者发送Abort
请求,通知它们放弃事务,参与者收到Abort
请求后回滚事务。
- DoCommit 阶段
- 如果协调者在
PreCommit
阶段后没有收到任何参与者的故障通知,它会向所有参与者发送DoCommit
请求,通知它们提交事务。参与者收到DoCommit
请求后,提交事务,将事务状态持久化到存储中。 - 如果协调者在
PreCommit
阶段后发现有参与者出现故障,它会向所有正常参与者发送Abort
请求,参与者收到Abort
请求后回滚事务。
三阶段提交的不同阶段设计对阻塞问题的优化体现在两方面:一是CanCommit
阶段比较轻量的资源检查和锁定操作,可以减轻冲突场景的中止回退开销,有点类似于try-lock
对lock
的优化;二是PreCommit
阶段的机制特点比二阶段提交的准备阶段灵活,不用阻塞等待协调者的指令,可以进行其他非事务操作,这也是参与者具备一定自治能力的体现。
对比二阶段提交,三阶段提交参与者自治性的来源在模型上可以理解为是从同步阻塞
模型向异步非阻塞
模型的转变。自然而然地,异步模型常用超时
来辅助管理状态驱动:
- 参与者在回应
CanCommit
请求后一定时间内没有收到协调者发送的下一个指令,可以直接中止本地事务,释放资源。 - 参与者在回应
PreCommit
请求后一定时间内没有收到协调者发送的下一个指令,可以直接中止或提交本地事务。实现方可以根据自身业务需要进行选择:中止策略数据副作用小,但浪费的概率大;提交策略的成立前提是CanCommit
的资源锁定有效,隔离等级可控,收到PreCommit
说明其他所有节点均已正常响应上一阶段,各自推进也能完成整体事务,并达到最终一致状态。
三阶段提交在一定程度上优化了二阶段提交的阻塞问题,对协调者和参与者发生故障的容忍度也更好,提高了系统的性能和容错能力。但实现难度较大,节点间需要转递的信息更多,对网络的要求也更高。
此外,基于超时的流程推进在极端场景可能会引发数据不一致问题,例如:超时回滚策略下,因网络隔离导致部分节点收到DoCommit
后提交事务,部分节点判断超时后回滚了事务;在超时提交策略下,协调者发出的Abort
指令慢于参与者的超时判断,导致该被中止的事务被提交了。
三阶段提交适用于对数据一致性要求较高,同时对系统的可用性和并发性能有一定要求的分布式系统,例如大规模分布式数据库系统和金融交易系统等。由于其复杂性,在一些简单的分布式场景可能不太适用,需要根据具体的业务需求和系统特点来选择。
TCC
2PC和3PC的设计考虑包含较强的数据库语义,适用于支持事务性操作的同构数据库之间的协同。如何事务涉及异构数据源,或者是分布式微服务之间的协同,或许就不堪合用了,需要考虑其他分布式事务模式。
TCC(Try-Confirm-Cancel)是一种分布式事务解决方案,旨在解决跨服务、跨资源的事务一致性问题。它通过业务逻辑层面的补偿机制,替代传统的数据库层事务,适用于高并发、高可用的分布式系统。

TCC将一个分布式事务拆分为三个阶段:
- Try(尝试):检查约束条件,预留资源,记录补偿操作。
- Confirm(确认):提交事务,执行实际业务逻辑。
- Cancel(取消):回滚事务,执行补偿操作,释放预留资源,恢复数据到事务前状态。
TCC乍看之下与3PC很相似,都有三个明确的阶段,有强一致性的意思。不过与3PC不同的是,TCC面向应用级别,缺乏类似数据库对事务性的支持,对资源的锁定、隔离没有底层保障,需要业务系统自行实现。由于没有强力约束,一般认为它是最终一致性的。
TCC的优势是弹性,几乎可以支持任何异构节点的事务性协作。弱点也是弹性,需要业务自行实现关键要点,否则难以取得想要的效果:
- 需要结合业务特点考虑事务隔离,注意预留资源与其他资源操作不产生冲突。例如库存管理增加预扣表,预留资源时转移实际库存到预扣表,以避*事务过程中其他操作对库存余量产生错误判断。
- 每个阶段都要保障幂等性,非同步阻塞的流程推进不可避*地会有重试。
- 要维护好状态机,处理指令乱序的情况,例如空回滚(
Try
未执行的Cancel
)和悬挂(Cancel
误覆盖Confirm
)问题。 - 要合理规划事务逻辑,尽量避*出现不可回退的操作,回滚失败的影响可能比执行失败更大。
- 建立适当的异常发现和人工介入机制。
Sagas
Sagas是一种将长事务分解为一系列短事务的方法,每个短事务都是一个可以单独提交或回滚的子事务,通过协调这些子事务来确保整个业务流程的一致性。
Sagas在一些方面解决了TCC未能很好解决的问题:
- 业务侵入性
- TCC:TCC对业务代码的侵入性相对较大,需要在业务方法中明确地实现
Try
、Confirm
和Cancel
三个方法,并且要处理好资源的预留、锁定和释放等逻辑。 - Sagas:Sagas对业务代码的侵入性相对较小,业务代码只需要关注自身的业务逻辑,不需要过多考虑事务的处理细节。
- 并发处理度
- TCC:TCC的
Try
阶段可能会对资源进行锁定,直到Confirm
或Cancel
阶段完成才释放锁,这在一定程度上会影响系统的并发性能。特别是在高并发场景,大量的资源被锁定可能导致线程阻塞,从而降低系统的吞吐量。 - Sagas:Sagas的子事务可以并行执行,以事务补偿代替资源锁定,在不考虑数据一致性问题的情况下,能显著提高系统的并发处理能力和性能。
- 补偿灵活性
- TCC:TCC的补偿操作(
Cancel
)通常是固定的回滚逻辑,对于一些复杂的业务场景,可能无法满足多样化的回滚需求。 - Sagas:Sagas的每个子事务都有对应的补偿事务,补偿逻辑可根据具体业务场景和事务状态灵活定制。例如发货失败时不固定执行回滚操作,而是采用部分退款、全额退款、提供优惠券等补偿行为。
Sagas的事务主要分为正向执行阶段(Action
)和反向补偿阶段(Compensate
)。

传统的Sagas是串行
模型:
- 外部请求触发第一个子事务的执行,前一个子事务的完成会触发下一个子事务的执行。在执行子事务的过程中,服务会发布相应的事件,其他服务通过监听这些事件来决定是否执行下一个子事务。
- 当某个子事务执行失败时,事务进入补偿阶段。按照子事务执行的相反顺序,依次调用已经执行成功的子事务的补偿操作。在补偿过程中,服务也会发布相应的补偿事件,其他服务通过监听这些事件来完成状态的恢复。
- 在节点故障、消息丢失等异常场景,需要协调者根据事务日志进行干涉,以推进事务进程。
相对的,可以同时执行一些不相干(interleaved)的事务,也就是并行
模型:

并行模型虽然可以提升性能和效率,但也大大地增加了实现复杂度,带来更多的取舍考量:
- 基于事件的驱动机制可能难以处理分组的并行依赖,需要强化协调者的作用,跟踪各个并行子事务的执行状态,并主动推进事务进程。
- 由于子事务是并行执行的,补偿操作的顺序可能无法严格按照执行的相反顺序,维护数据一致性的难度变大,要根据具体的业务逻辑和数据依赖关系来确定。
- 多个子事务并行执行可能会导致对共享资源的竞争,如果没有正确地处理资源的访问控制,可能会出现数据冲突、死锁等问题,影响系统的稳定性和可靠性。