前言

最近在看分布式事务相关的实现方案,涉及到的模式比较多,觉得有必要梳理下,用于后续涉及到分布式事务使用时的方法决策。
基于个人理解,本文的模式主要分两类:理论型和实践型

  • 理论型:经典的两阶段提交(XA)、经典的TCC模式,理论型更多的是作为实践型的思想
  • 实践型:又可以分为不回滚主事务型(本地消息表、事务消息、最大努力通知),以及可回滚主事务型(Seata的AT TCC及saga、以及蚂蚁的DTS模式)

本文主要是对各种模式的概念及使用做一个入门。限于个人认知,如有描述不正确的地方欢迎指正。

经典的两阶段提交(XA)

XA 的全称是 eXtended Architecture,由X/Open组织提出,是关于分布式事务处理 (DTP)的规范。
XA通过二阶段提交协议保证强一致性,中间的操作要么全部成功,要么全部失败。
大致的流程:

  • 第一阶段(prepare):事务管理器向所有本地资源管理器发起请求,询问是否是 ready 状态,所有参与者都将本事务能否成功的信息反馈发给协调者;第一阶段需要做资源的预锁定,手段可以是在表中增加预锁定字段,如转账场景下的冻结金额
  • 第二阶段 (commit/rollback):事务管理器根据所有本地资源管理器的反馈,先在本地持久化事务状态,然后通知所有本地资源管理器,步调一致地在所有分支上提交或者回滚。commit的话,资源管理器正式完成操作,并释放在整个事务期间占用的资源,如冻结金额变成正式扣款;rollback的话,如冻结金额扣掉回滚的金额。

存在的问题:

  • 同步阻塞:当参与事务者存在占用公共资源的情况,其中一个占用了资源,其他事务参与者就只能阻塞等待资源释放,处于阻塞状态。
  • 单点故障:一旦事务管理器出现故障,整个系统不可用
  • 数据不一致:在阶段二,如果事务管理器只发送了部分 commit 消息,此时网络发生异常,那么只有部分参与者接收到 commit 消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。
  • 不确定性:当协事务管理器发送 commit 之后,并且此时只有一个参与者收到了 commit,那么当该参与者与事务管理器同时宕机之后,重新选举的事务管理器无法确定该条消息是否提交成功。

以上是经典的两阶段提交,而现在工程实践中使用到的模式大多基于两阶段提交的思想再做优化,如蚂蚁DTS、seata at、seata tcc等。两阶段提交的思想简单来说就是第一阶段先预占/锁定,第二阶段进行提交或回滚。

经典的TCC

TCC(Try-Confirm-Cancel)类似两阶段提交,相比之下没有两阶段定义的资源管理器等标准概念,但思想是类似的,都是先锁定,再提交/回滚。基于TCC实现分布式事务,需要将事务实现的逻辑拆分为 Try、Confirm、Cancel 三个接口。

机制流程:

  • Try 阶段:尝试执行,完成所有业务检查(一致性), 预留必须业务资源(准隔离性),手段可以是在表中增加预锁定字段,如转账场景下的冻结金额
  • Confirm 阶段:确认执行真正执行业务,不作任何业务检查,只使用 Try 阶段预留的业务资源,如冻结金额变成正式扣款。Confirm 操作满足幂等性。要求具备幂等设计,Confirm 失败后需要进行重试
  • Cancel 阶段:取消执行,如冻结金额扣掉回滚的金额。释放 Try 阶段预留的业务资源 Cancel 操作满足幂等性 Cancel 阶段的异常和 Confirm 阶段异常处理方案基本上一致。

相比XA优点:

  • 解决了协调者单点:由主业务方发起并完成这个业务活动,跟着主业务方变成多个节点
  • 解决同步阻塞:引入超时,超时后进行补偿,并且不会锁定整个资源,将资源转换为业务逻辑形式,粒度变小。
  • 数据一致性:有了补偿机制之后,由业务活动管理器控制一致性

本地消息表

image.png

一般的流程:

  1. 当系统 A 被其他系统调用发生数据库表更操作,首先会更新数据库的业务表,其次会往相同数据库的消息表中插入一条数据,两个操作发生在同一个事务中
  2. 系统 A 的脚本定期轮询本地消息往 mq 中写入一条消息,如果消息发送失败会进行重试
  3. 系统 B 消费 mq 中的消息,并处理业务逻辑。如果本地事务处理失败,会在继续消费 mq 中的消息进行重试,如果业务上的失败,可以通知系统 A 进行回滚操作

本地消息表实现的条件:

  • 消费者与生成者的接口都要支持幂等
  • 生产者需要额外的创建消息表

容错机制:

  • 步骤 1 失败时,事务直接回滚
  • 步骤 2、3 写 mq 与消费 mq 失败会进行重试

该模式一般用于先保证主事务(系统A)成功,再异步去保证从事务(系统B)成功。也就是从事务发生业务失败,主事务也不回滚。
若有回滚的需求,可以通过修改消息状态去标识,但需要有额外的机制去保证从事务写消息状态成功。

事务消息

image.png
事务消息发送步骤如下:

  1. 消息队列服务端将消息持久化成功之后,向发送方返回Ack确认消息已经发送成功,此时消息为半事务消息。
  2. 发送方开始执行本地事务逻辑。
  3. 发送方根据本地事务执行结果向服务端提交二次确认(Commit或是Rollback),服务端收到Commit状态则将半事务消息标记为可投递,订阅方最终将收到该消息;服务端收到Rollback状态则删除半事务消息,订阅方将不会接受该消息。

事务消息回查步骤如下:

  1. 在断网或者是应用重启的特殊情况下,上述图中步骤4提交的二次确认最终未到达服务端,经过固定时间后服务端将对该消息发起消息回查。
  2. 发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
  3. 发送方根据检查得到的本地事务的最终状态再次提交二次确认,服务端仍按照图中步骤4对半事务消息进行操作。

核心是两阶段提交(第一阶段半事务、第二阶段commit/rollback),以及回查事务状态的特性
这个模式适用于只保证消息发送方事务成功的场景,如果订阅方发生业务失败回滚了,发送方也不会回滚。
实现此模式的如RocketMQ

最大努力通知

最大努力通知是最简单的一种柔性事务,适用于一些最终一致性时间敏感度低的业务,且被动方处理结果不影响主动方的处理结果、且不保证被动方一定能收到通知进行处理。
执行流程:

  1. 系统A本地事务执行完之后,发送个消息到 MQ
  2. 系统B消费MQ进行处理;
  3. 要是系统B执行失败了,那么利用MQ进行重试,反复N次,最后还是不行就放弃

这里第一步可能在发MQ的时候失败,但不做处理。

Seata AT(Automatic Transaction)

image.png
采用两阶段提交的思想,如下

  • 一阶段 prepare 行为:在本地事务中,同时提交业务数据更新和相应回滚日志记录的事务。
  • 二阶段 commit 行为:变更事务状态为结束,同时异步批量清理回滚日志。
  • 二阶段 rollback 行为:通过回滚日志,自动生成补偿操作,完成数据回滚。

注意,每个阶段针对的是链路下的所有操作。比如在下单链路中,增加订单、扣件库存两个不同服务的操作,那么一阶段会在不同服务实现中的DB操作中记录回滚日志记录等操作。

AT的Automatic体现在,对于业务来说记录回滚日志、清理回滚日志、通过回滚日志补偿等操作都是自动进行的,不需要去关注这些逻辑。

AT模式具有写隔离及读隔离的特性:

  • 写隔离指的是不会发生脏写的情况,简单来说就是事务A和B更新同一行记录,A先执行B后执行,此时A发生回滚,B不会基于A回滚前的数据进行操作,B只会基于A最终提交的数据进行操作
  • 读隔离指的是读到的数据都是已提交的,对应到数据库隔离级别的“已提交读”

该模式的特点如下:

  • 各个分支事务都拥有回滚能力
  • 接入简单,无业务侵入,不需要关注回滚逻辑的实现,由AT实现
  • 通过全局锁,具备事务隔离型RC
  • 强一致性

Seata TCC

image.png
采用两阶段提交思想,相比at模式,依赖于用户自行实现三个方法,即需要实现预提交、提交、回滚的能力
demo见https://github.com/seata/seata-samples/blob/master/tcc/dubbo-tcc-sample/README.MD

相比经典的TCC,经典的TCC是偏理论的概念,而Seata TCC是工程上的框架,可直接使用。

该模式的特点如下:

  • 各个分支事务拥有回滚能力
  • 自定义三个阶段的逻辑实现,实现复杂,但在监控、数据库中间件的使用上更灵活,可以结合项目的技术栈进行使用
  • 需自行实现隔离性
  • 最终一致性
  • 对业务有侵入,并需要考虑空回滚、防悬挂等情况

Seata saga

image.png
saga是长事务解决方案,使用的时候需要实现事务逻辑和回滚补偿逻辑。
如图有n个事务,当T3失败的时候,会沿着C3(Compensation3)回滚补偿T1, T2, T3对应的补偿逻辑。

基于状态机实现的saga,需要定义状态编排,如图有ReduceInventory和ReduceBalance的事务逻辑,并且有对应的补偿逻辑,当失败的时候就会执行。
image.png
saga分client和server端,上面的为client端(即本地),client端需要记录操作日志,当出现宕机时,由server触发事务恢复,本地通过日志来恢复/补偿事务。

该模式的特点如下:

  • 各个分支事务都拥有回滚能力
  • 适用于分布式事务流程长的场景
  • 如果分布式事务涉及到旧逻辑,可能难以进行补偿逻辑的实现
  • 不保证事务隔离性
  • 对业务有侵入,并需要考虑空回滚、防悬挂等情况
  • 最终一致性

蚂蚁DTS

采用两阶段提交思想,DTS 从架构上分为 xts-client 和 xts-server 两部分,前者是一个嵌入客户端应用的 Jar 包,主要负责事务数据的写入和处理;后者是一个独立的系统,主要负责异常事务的恢复。
image.png
发起方会有一张表,记录事务过程,用于标记事务是否成功和判断是否需要回滚
server端为恢复系统。如果二阶段失败,比如需要 commit app2 和 app3,如果 commit app2 的时候断电了,通过我们的 xts-server 这个恢复系统来保证事务一定会被提交/回滚。在某些特殊情况下(比如断电,jvm crash等导致分布式事务没有处理完就结束了),xts-server 靠持久化记录到 db 的事务数据来完成恢复。这个db为发起方的db
分布式事务整体的执行流程如下:

  1. 发起方插入db事务记录,准备开始事务
  2. prepare app2 app3,并对应插入action记录
  3. 成功则commit,失败则rollback
  4. 若2、3失败,则通过db判断事务是否完成,来决定commit/rollback
  5. 通过server端可以定时做commit/rollback补偿

该模式更多介绍见:https://tech.antfin.com/docs/2/46887

参考链接