分布式事务实现

这篇总结各种常见分布式事务的实现方式,有很多技术细节,但是没有提太多具体实现,之后有时间再梳理一下tcc-transaction这些框架。

XA规范

X/Open 组织(即现在的 Open Group )定义了分布式事务处理模型。 X/Open DTP 模型( 1994 )包括应用程序( AP )、事务管理器( TM )、资源管理器( RM )、通信资源管理器( CRM )四部分。一般,常见的事务管理器( TM )是交易中间件,常见的资源管理器( RM )是数据库,常见的通信资源管理器( CRM )是消息中间件。
通常把一个数据库内部的事务处理,如对多个表的操作,作为本地事务看待。数据库的事务处理对象是本地事务,而分布式事务处理的对象是全局事务。
所谓全局事务,是指分布式事务处理环境中,多个数据库可能需要共同完成一个工作,这个工作即是一个全局事务,例如,一个事务中可能更新几个不同的数据库。对数据库的操作发生在系统的各处但必须全部被提交或回滚。此时一个数据库对自己内部所做操作的提交不仅依赖本身操作是否成功,还要依赖与全局事务相关的其它数据库的操作是否成功,如果任一数据库的任一操作失败,则参与此事务的所有数据库所做的所有操作都必须回滚。 一般情况下,某一数据库无法知道其它数据库在做什么,因此,在一个 DTP 环境中,交易中间件是必需的,由它通知和协调相关数据库的提交或回滚。而一个数据库只将其自己所做的操作(可恢复)影射到全局事务中。

XA 就是 X/Open DTP 定义的交易中间件与数据库之间的接口规范(即接口函数),交易中间件用它来通知数据库事务的开始、结束以及提交、回滚等。 XA 接口函数由数据库厂商提供。

二阶提交协议和三阶提交协议就是根据这一思想衍生出来的。可以说二阶段提交其实就是实现XA分布式事务的关键(确切地说:两阶段提交主要保证了分布式事务的原子性:即所有结点要么全做要么全不做)

2PC(Two-phaseCommit)

二阶段提交(Two-phaseCommit)是指,在计算机网络以及数据库领域内,为了使基于分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种算法(Algorithm)。通常,二阶段提交也被称为是一种协议(Protocol))。在分布式系统中,每个节点虽然可以知晓自己的操作时成功或者失败,却无法知道其他节点的操作的成功或失败。当一个事务跨越多个节点时,为了保持事务的ACID特性,需要引入一个作为协调者的组件来统一掌控所有节点(称作参与者)的操作结果并最终指示这些节点是否要把操作结果进行真正的提交(比如将更新后的数据写入磁盘等等)。因此,二阶段提交的算法思路可以概括为:参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作

准备阶段(投票阶段)

事务协调者(事务管理器)给每个参与者(资源管理器)发送Prepare消息,每个参与者要么直接返回失败(如权限验证失败),要么在本地执行事务,写本地的redo和undo日志,但不提交,到达一种“万事俱备,只欠东风”的状态。
可以进一步将准备阶段分为以下三个步骤:

  1. 协调者节点向所有参与者节点询问是否可以执行提交操作(vote),并开始等待各参与者节点的响应。
  2. 参与者节点执行询问发起为止的所有事务操作,并将Undo信息和Redo信息写入日志。(注意:若成功这里其实每个参与者已经执行了事务操作)
  3. 各参与者节点响应协调者节点发起的询问。如果参与者节点的事务操作实际执行成功,则它返回一个”同意”消息;如果参与者节点的事务操作实际执行失败,则它返回一个”中止”消息。

提交阶段(执行阶段)

如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。(注意:必须在最后阶段释放锁资源)
接下来分两种情况分别讨论提交阶段的过程。
当协调者节点从所有参与者节点获得的相应消息都为”同意”时:
2PC提交

  1. 协调者节点向所有参与者节点发出”正式提交(commit)”的请求。
  2. 参与者节点正式完成操作,并释放在整个事务期间内占用的资源。
  3. 参与者节点向协调者节点发送”完成”消息。
  4. 协调者节点受到所有参与者节点反馈的”完成”消息后,完成事务。

如果任一参与者节点在第一阶段返回的响应消息为”中止”,或者 协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时:
2PC回滚

  1. 协调者节点向所有参与者节点发出”回滚操作(rollback)”的请求。
  2. 参与者节点利用之前写入的Undo信息执行回滚,并释放在整个事务期间内占用的资源。
  3. 参与者节点向协调者节点发送”回滚完成”消息1. 协调者节点受到所有参与者节点反馈的”回滚完成”消息后,取消事务。
    不管最后结果如何,第二阶段都会结束当前事务。

优点

确实能保证事务的原子性

缺点

2PC异常点
图中有问号的条目,是不确定的地方,但是不影响这个分布式事务的结果
图中的感叹号条目,个人感觉其实也是允许先发消息再记录日志的,但是如果这样子做以后发生Down机,客户端或者TM都需要向其它机器询问结果才能得到结论(而这样子做的话会大大加长分布事务的阻塞时间和事务处理的复杂度,同时这样做会有一个致命的缺陷,抹除了一部分可以自恢复场景。

  1. 同步阻塞问题
    执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。具体地说,两阶段提交中的第二阶段,协调者需要等待所有参与者发出yes请求,或者某个参与者发出no请求后,才能执行提交或者中断操作。这会造成长时间同时锁住多个资源, 造成性能瓶颈, 如果参与者有一个耗时长的操作, 性能损耗会更明显.
  2. 单点故障
    由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)
  3. 数据不一致/脑裂(聋哑事件)
    在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。此时就算重新选举出了一个新的协调者,也没有人知道系统最后的状态。而在这部分参与者接到commit请求之后就会执行commit操作,但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据不一致性现象。
  4. 实现复杂
    不利于系统的扩展。
  5. 二阶段无法解决的问题
    协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。

3PC(Three-phase commit)

三阶段提交,也叫三阶段提交协议(Three-phase commit protocol),是二阶段提交(2PC)的改进版本。

与两阶段提交不同的是,三阶段提交有两个改动点。

  1. 引入超时机制。同时在协调者和参与者中都引入超时机制。
  2. 在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。

也就是说,除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。
3PC

CanCommit阶段

3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。

  1. 事务询问 协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应。
  2. 响应反馈 参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则反馈No

PreCommit阶段

协调者根据参与者的反应情况来决定是否可以进行事务的PreCommit操作。根据响应情况,有以下两种可能,分别称为预执行和中断。
假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行

  1. 发送预提交请求:协调者向参与者发送PreCommit请求,并进入Prepared阶段。
  2. 事务预提交:参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中。
  3. 响应反馈:如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。

假如有任何一个参与者向协调者发送了No响应(CanCommit阶段),或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断

  1. 发送中断请求 协调者向所有参与者发送abort请求。
  2. 中断事务 参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。

doCommit阶段

该阶段进行真正的事务提交,也可以分为以下两种情况。
执行提交

  1. 发送提交请求 协调接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送doCommit请求。
  2. 事务提交 参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。
  3. 响应反馈 事务提交完之后,向协调者发送Ack响应。
  4. 完成事务 协调者接收到所有参与者的ack响应之后,完成事务。

中断事务
协调者没有接收到参与者发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超时),那么就会执行中断事务。

  1. 发送中断请求 协调者向所有参与者发送abort请求
  2. 事务回滚 参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。
  3. 反馈结果 参与者完成事务回滚之后,向协调者发送ACK消息
  4. 中断事务 协调者接收到参与者反馈的ACK消息之后,执行事务的中断。

在doCommit阶段,如果参与者无法及时接收到来自协调者的doCommit或者rebort请求时,会在等待超时之后,会继续进行事务的提交。(其实这个应该是基于概率来决定的,当进入第三阶段时,说明参与者在第二阶段已经收到了PreCommit请求,那么协调者产生PreCommit请求的前提条件是他在第二阶段开始之前,收到所有参与者的CanCommit响应都是Yes。(一旦参与者收到了PreCommit,意味他知道大家其实都同意修改了)所以,一句话概括就是,当进入第三阶段时,由于网络超时等原因,虽然参与者没有收到commit或者abort响应,但是他有理由相信:成功提交的几率很大。 )

优点和缺点

  1. 相对于2PC,3PC主要解决的单点故障问题
    当在第二阶段出现聋哑事件,那么这N-1台机器可以根据超时机制直接abort掉,因为客户端这时只是预提交,当该机器重启以后只要询问周边机器事务状态,简单的将事务回滚或者提交事务,就能保持事务的最终一致性;
    当进行到第三阶段的时候,如果发生聋哑事件,那么其它处于「不确定状态」的客户端会直接执行commit,而不会像2PC一样导致事务block,但是这样会有一个风险(进入到第三个阶段说明客户端在第一阶段投的都是Yes),因为在聋哑事件中,那台Down掉的机器在第二阶段中给协调者发送的不是prepared,这个时候协调者收到消息给客户端发送的是abort命令.所以3PC只是乐观的认为只要你第一阶段大家投的都是Yes,那么最后成功提交的几率很大。
  2. 减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit,而不会一直持有事务资源并处于阻塞状态。
  3. 但是这种机制也会导致数据一致性问题,因为,由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。

TCC

TCC
所谓的TCC编程模式,也是两阶段提交的一个变种。TCC提供了一个编程框架,将整个业务逻辑分为三块:Try、Confirm和Cancel三个操作。以在线下单为例,Try阶段会去扣库存,Confirm阶段则是去更新订单状态,如果更新订单失败,则进入Cancel阶段,会去恢复库存。总之,TCC就是通过代码人为实现了两阶段提交,不同的业务场景所写的代码都不一样,复杂度也不一样,因此,这种模式并不能很好地被复用。
TCC补偿性方案,分为三个阶段TRYING-CONFIRMING-CANCELING。每个阶段做不同的处理。

  • Trying阶段主要针对业务系统检测及作出预留资源请求, 若预留资源成功, 则返回确认资源的链接与过期时间
  • Confirm阶段主要是对业务系统的预留资源作出确认, 要求TCC服务的提供方要对确认预留资源的接口实现幂等性, 若Confirm成功则返回204,资源超时则证明已经被回收且返回404
  • Cancel阶段主要是在业务执行错误或者预留资源超时后执行的资源释放操作, Cancel接口是一个可选操作, 因为要求TCC服务的提供方实现自动回收的功能, 所以即便是不认为进行Cancel, 系统也会自动回收资源

优点

  1. TCC能够对分布式事务中的各个资源进行分别锁定, 分别提交与释放, 例如, 假设有AB两个操作, 假设A操作耗时短, 那么A就能较快的完成自身的try-confirm-cancel流程, 释放资源. 无需等待B操作. 如果事后出现问题, 追加执行补偿性事务即可.
  2. TCC是绑定在各个子业务上的(除了cancle中的全局回滚操作), 也就是各服务之间可以在一定程度上”异步并行”执行.

注意

  • 事务管理器(协调器)这个节点必须以带同步复制语义的高可用集群(HAC)方式部署.
  • 事务管理器(协调器)还需要使用多数派算法来避免集群发生脑裂问题.

适用场景

  • 严格一致性
  • 执行时间短
  • 实时性要求高

例子1

支付场景为例,支付系统接收到会员的支付请求后,需要扣减会员账户余额、增加会员积分(暂时假设需要同步实现)增加商户账户余额
再假设:会员系统、商户系统、积分系统是独立的三个子系统,无法通过传统的事务方式进行处理。

  1. TRYING阶段:我们需要做的就是会员资金账户的资金预留,即:冻结会员账户的金额(订单金额)
  2. CONFIRMING阶段:我们需要做的就是会员积分账户增加积分余额,商户账户增加账户余额
  3. CANCELING阶段:该阶段需要执行的就是解冻释放我们扣减的会员余额

例子2

下面以客户购买商品时的付款操作为例进行讲解:

  1. Try
    完成所有的业务检查(一致性),预留必须业务资源(准隔离性);
    体现在本例中, 就是确认客户账户余额足够支付(一致性), 锁住客户账户, 商户账户(准隔离性).
  2. Confirm
    使用Try阶段预留的业务资源执行业务(业务操作必须是幂等的), 如果执行出现异常, 要进行重试.
    在这里就是执行客户账户扣款, 商户账户入账操作.
  3. Cancle
    释放Try阶段预留的业务资源, 在这里就是释放客户账户和商户账户的锁;

try设计

try阶段起着一个预处理的作用。
在整个分布式事务中预处理的含义其实很广泛,比如订单,所谓的预处理就是生成订单,但是用户真实是看不到这些订单的,至于具体实现是在一张新表中记录还是在原有的订单表是加上标记位,具体实现方式由自己统筹考虑(当然还需要考虑记录事务Id);像减库存这种预处理,可以直接减少原始库存,再通过另外一张表来记录这次事务Id操作了哪个Sku的库存数量,当然也可以不减少库存只记录操作,但是这种方式在计算实际库存的时候复杂度会提高(需要减掉预处理的那部分)

confirm设计

如果任一子业务在Confirm阶段有操作无法执行成功, 会造成对业务活动管理器的响应超时, 此时要对其他业务执行补偿性事务. 如果补偿操作执行也出现异常, 必须进行重试, 若实在无法执行成功, 则事务管理器必须能够感知到失败的操作, 进行log(用于事后人工进行补偿性事务操作或者交由中间件接管在之后进行补偿性事务操作),我称这个过程为Unconfirm.
其实像Github上的一些基于TCC设计的分布式事务框架(如tcc-transaction),都对Confirm的补偿没有作说明,也没有在代码里保证某一子任务的Confirm失败后、前面子任务的Confirm能回滚,Confirm操作出错是由用户承担的,经过测试确实会有这样的问题(在第一个Confirm中建一张表,第二个Confirm中故意抛出一个异常)。
因此,我认为TCC应该被扩充为TCUC才对,其中的U代表Unconfirm操作。

cancel设计

cancel应当能起到释放try阶段占用资源的作用,事务管理器在执行cancel时需要判断哪些try是成功的再执行其cancel,因为执行失败的try由本地事务控制回滚了,而没有执行的本来就没有必要回滚,或者,由开发者将cancel设计为可重入的、不会因为反复调用而出错。但是由于这个需求是刚需,放到业务中进行处理势必会大大增加业务的复杂度,因此由TCC框架来处理是更好的选择,需要考虑如下处理策略:

  1. 如果TCC事务框架发现某个服务的Try操作的本地事务尚未提交,应该直接将其回滚,而后就不必再执行该服务的cancel业务;
  2. 如果TCC事务框架发现某个服务的Try操作的本地事务已经回滚,则不必再执行该服务的cancel业务;
  3. 如果TCC事务框架发现某个服务的Try操作尚未被执行过,那么,也不必再执行该服务的cancel业务。

总而言之,TCC框架必须保障:

  1. 已生效的Try操作应该被其Cancel操作所回撤;
  2. 尚未生效的Try操作,则不应该执行其Cancel操作。

本地事务

TCC事务必须在本地事务的基础上实现,因为每个接口都可能有多次写库操作,如果某次写库失败,cancel中就需要判断哪些操作是失败的再调用其回滚,这样cancel中也会存在多次的反向写库操作,一旦cancel也中途出错,后续的cancel(重试)还需要判断之前cancel的哪几个操作是执行成功了的,因此,如果没有本地事务的支持,TCC事务框架是无法有效管理TCC事务的。
一种方法是在TCC框架中接管Spring的事务管理(PlatformTransactionManager),另一种办法是老老实实给每一个TCC接口添加@Transactional注解。

  1. 必须确定本地事务的传播条件
    一个业务方法可能会包含多个RM本地事务。比如:A(REQUIRED)->B(REQUIRES_NEW)->C(REQUIRED),这种情况下,A服务所参与的RM本地事务被提交时,B服务和C服务参与的RM本地事务则可能会被回滚。
  2. 必须手动指定本地事务的回滚条件
    并不是抛出了异常的业务方法,其参与的事务就回滚了。Spring容器的声明式事务定义了两类异常,其事务完成方向都不一样:系统异常(一般为Unchecked异常,默认事务完成方向是rollback)、应用异常(一般为Checked异常,默认事务完成方向是commit)。二者的事务完成方向又可以通过@Transactional配置显式的指定,如rollbackFor/noRollbackFor等。
    Spring容器还支持使用setRollbackOnly的方式显式的控制事务完成方向。
  3. TCC拦截器的执行顺序必须在本地事务拦截器之后
    自行拦截业务方法的拦截器和Spring的事务处理的拦截器还会存在执行先后、拦截范围不同等问题。例如,如果自行拦截器执行在前,就会出现业务方法虽然已经执行完毕但此时其参与的RM本地事务还没有commit/rollback。

异常分析

实际应用中,会有各种故障出现,很多都会造成事务的中断,从而使得统一提交/回滚全局事务的目标不能达到,甚至出现”一部分分支事务已经提交,而另一部分分支事务则已回滚”的情况。常见错误包括:
业务系统服务器宕机、重启;数据库服务器宕机、重启;网络故障;断电等。这些故障可能单独发生,也可能会同时发生。

在整个流程,我们主要需要关注的是cancel失败和confirm失败引起的数据不一致现象:
TCC异常分析
TCC事务框架要支持故障恢复,就必须记录相应的事务日志。事务日志是故障恢复的基础和前提,它记录了事务的各项数据。TCC事务框架做故障恢复时,可以根据事务日志的数据将中断的事务恢复至正确的状态,并在此基础上继续执行先前未完成的提交/回滚操作。

异常情况 - Cancel与Try乱序(或并发执行)

这应该算TCC事务机制特有的一个不可思议的陷阱。一般来说,一个特定的TCC服务,其Try操作的执行,是应该在其Confirm/Cancel操作之前的。Try操作执行完毕之后,Spring容器再根据Try操作的执行情况,指示TCC事务框架提交/回滚全局事务。然后,TCC事务框架再去逐个调用各TCC服务的Confirm/Cancel操作。
然而,超时、网络故障、服务器的重启等故障的存在,使得这个顺序会被打乱。比如:
Cancel与Try乱序
上图中,假设[B:Try]操作执行过程中,网络闪断,[A:Try]会收到一个RPC远程调用异常。A不处理该异常,导致全局事务决定回滚,TCC事务框架就会去调用[B:Cancel],而此刻A、B之间网络刚好已经恢复。如果[B:Try]操作耗时较长(网络阻塞/数据库操作阻塞),就会出现[B:Try]和[B:Cancel]二者并行处理的现象,甚至[B:Cancel]先完成的现象。
这种情况下,由于[B:Cancel]执行时,[B:Try]尚未生效(其RM本地事务尚未提交),因此,[B:Cancel]是不能执行的,至少是不能生效(执行了其RM本地事务也要rollback)的。
然而,当[B:Cancel]处理完毕(跳过执行、或者执行后rollback其RM本地事务)后,[B:Try]操作完成又生效了(其RM本地事务成功提交),这就使得[B:Cancel]虽然提供了,但却没有起到回撤[B:Try]的作用,导致数据的不一致。

所以,TCC框架在这种情况下,需要:

  1. 将[B:Try]的本地事务标注为Marked_ReadOnly,阻止其后续生效;
  2. 禁止其再次将事务上下文传递给其他远程分支,否则该问题将在其他分支上出现;
  3. 相应地,[B:Cancel]也不必执行,至少不能生效。

    当然,TCC事务框架也可以简单的选择阻塞[B:Cancel],待[B:Try]执行完毕后,再根据它的执行情况判断是否需要执行[B:Cancel]。不过,这种处理方式因为需要等待,所以,处理效率上会有所不及。

同样的情况也会出现在confirm业务上,只不过,发生在Confirm业务上的处理逻辑与发生在Cancel业务上的处理逻辑会不一样,TCC框架必须保证:

  1. Confirm业务在Try业务之后执行,若发现并行,则只能阻塞相应的Confirm业务操作;
  2. 在进入Confirm执行阶段之后,也不可以再提交同一全局事务内的新的Try操作的RM本地事务。

TCC中心化(使用一个TCC服务器来集中回调confirm和cancel方法)

基于TCC的中心化事务一致性解决方法,各个应用服务器如果需要感知某次事务是否成功的成本很高,所以对于自身而言进行事务补偿成本就会很高.举个例子:
TCC中心化

  1. 每次成功的执行本应用服务器的事务以后,都需要把成功执行的事务Id记录
  2. 继续confirm或者将confirm完的数据回滚,对用户都很不友好,特别是需要confirm订单或者回滚订单数据
  3. 可以根据事务开始的时间,并且设计一个事务超时时间,如果在这个时间范围以外事务还没有处理完成,就可以当做这个事务已经失败,将预处理数据删除
    总体来说,事务补偿机制,心智负担过于沉重.所以只能依赖TCC服务器的失败重试机制,如果失败重试机制不能处理,只能人肉去处理(建议全程人肉,因为同时进行失败重试和人肉的话,因为如果失败重试和人肉都在操作同一条数据,还需要考虑这种竞争的场景,对重试次数需要限定)

无TCC服务器设计(去中心化)

可以让交易链路来充当TCC服务器的角色,但是长期来看,TCC相当于是一个公用的组件,所以其它地方也需要TCC分布式事务,可以公用这一个组件(交易链路可以完成TCC所能完成的一切操作,把TCC单独部署一个服务,仅仅是考虑整个系统的抽象结构和功能复用)。
像框架tcc-transaction中每次第一个发起事务的服务器就起到了这个TCC服务器的作用。

幂等性

TCC服务支持接口失败重试,所以对TCC暴露的接口都需要满足幂等性(根据事务Id很好满足),幂等性的实现方式可以是:

  1. 通过唯一键值做处理,即每次调用的时候传入唯一键值,通过唯一键值判断业务是否被操作,如果已被操作,则不再重复操作
  2. 通过状态机处理,给业务数据设置状态,通过业务状态判断是否需要重复执行

调用链路

就想本地事务通过方法调用来传递,分布式事务也需要在进行远程调用时传递该事务的标识(称为调用链路ID,有时会包装到一个事务上下文内)
服务为了确认自己处于哪个事务中,必须将调用链路ID作为参数在远程调用时进行传递,这和实现密切相关,待补充。。。。

结合MQ消息中间件实现的可靠消息最终一致性

可靠消息最终一致性,需要业务系统结合MQ消息中间件实现,在实现过程中需要保证消息的成功发送及成功消费。即需要通过业务系统控制MQ的消息状态。
所谓的消息事务本质上就是基于消息中间件的两阶段提交,是对消息中间件的一种特殊利用,它是将本地事务和发消息放在了一个分布式事务里,保证要么本地操作成功成功并且对外发消息成功,要么两者都失败,开源的RocketMQ就支持这一特性,具体原理如下:
消息事务
步骤1失败:则整个事务失败,不会执行A的本地操作;
步骤2失败:同样整个事务失败,不会执行A的本地操作(被本地数据库回滚掉了);
步骤3失败:消息中间件需要有回查机制,回查A系统该事务是否被本地执行成功了,如果成功则照常继续执行,如果失败则将预备消息回滚了,其实这指的就是步骤4的回调。

虽然上面的方案能够完成A和B的操作,但是A和B并不是严格一致的,而是最终一致的,我们在这里牺牲了一致性,换来了性能的大幅度提升。当然,这种玩法也是有风险的,如果B一直执行不成功,那么一致性会被破坏,具体要不要玩,还是得看业务能够承担多少风险。

实例 - 下单

消息事务-下单

  1. 预创建订单失败:如果实际预创建订单成功,订单定时补偿机制,定时删除这部分订单,不影响数据一致性,下单失败;
  2. 预扣减库存失败:如果预扣减库存真实失败,则下单失败(订单由定时补偿机制定时删除,其它应用参照场景4的处理方式,下单失败;如果实际预扣减库存成功,参照场景4的处理方式,下单失败;
  3. 实际创建订单失败:如果创建订单真实失败(不需要发送下单失败消息,防止实际创建订单成功场景),订单的预处理数据通过订单的定时补偿机制尝试删除(需要考虑事务处理时间,将超过某个时间范围该事务还处于预处理状态的订单删除),下单失败;如果实际创建订单成功,其它应用参照场景4的处理方式,下单成功(提示用户下单失败);
  4. 发送订单创建成功消息失败/库存服务由于各种原因没有接到下单成功消息:库存服务定时轮询处理数据(需要考虑事务处理时间,将超过某个时间范围该事务还处于预处理状态的订单筛选出来),询问订单服务改订单Id对应的订单是否创建成功,根据订单创建成功与否选取相应的事务补偿机制

和TCC比较

  1. TCC是把所有的订单创建步骤平等看待,只要有一个失败,整个下单流程全部失败(比较TCC里面的confirm失败和基于MQ实际创建订单失败的补偿难易程度)
  2. TCC是通过发消息给TCC服务器,然后由TCC服务调用应用服务;
    基于MQ的分布式事务补偿机制,是通过将消息发送到MQ,然后由应用自己去监听MQ的事件。

注意

消息中间件在系统中扮演一个重要的角色, 所有的事务消息都需要通过它来传达, 所以消息中间件也需要支持 HA 来确保事务消息不丢失.
根据业务逻辑的具体实现不同,还可能需要对消息中间件增加消息不重复, 不乱序等其它要求.

适用场景

  1. 执行周期较长
  2. 实时性要求不高

例如:

  • 跨行转账/汇款业务(两个服务分别在不同的银行中)
  • 退货/退款业务
  • 财务, 账单统计业务(先发送到消息中间件, 然后进行批量记账)