常见的分布式事务详解

1. 简介

微服务是由许多较小的服务组成的分布式系统,它们共同提供整体应用功能。尽管这种架构风格具有很多优点,但也存在一些缺点,其中最大的问题之一就是如何管理涉及多个服务的事务。

在分布式事务场景中,尽管事务涉及多个服务,但始终要保持ACID(原子性、一致性、隔离性和持久性)的特性。第二个困难是控制事务的隔离级别,它决定了在其他服务同时访问相同数据时,事务中数据的可见性。

分布式事务:在分布式系统中一次操作需要多个服务协同完成,这种由不同的服务之间通过网络协同完成的事务称为分布式事务

2. 2PC

2PC(Two Phase Commitment Protocol)是一种常用的实现分布式事务的方法。它包括一个协调器组件负责控制事务并包含事务逻辑,以及其他参与节点执行其本地事务。

2PC将整个事务流程分为两个阶段:准备阶段(Prepare phase)、提交阶段(Commit phase);

2.1 准备阶段

准备阶段(Prepare phase):由协调器给每个参与者发送Prepare消息,每个数据库参与者在本地执行事务,此时事务没有提交

  • 协调器项所有参与者发送事务内容,询问是否可以提交事务,并等待答复

  • 各参与者执行本地事务操作,将Undo Log/Redo Log记入事务日志中,但不提交事务

  • 如果参与者执行成功,给协调器反馈统一,否则反馈终止,表示事务不可以执行

2.2 提交阶段

提交阶段(Commit phase): 协调器收到参与者的失败或者超时消息后,直接给每个参与者发送回滚(Rollback)消息,否则,发送提交(commit)消息;参与者根据协调器的指令执行提交或者回滚操作

  1. 事务提交

    • 协调器项所有参与者节点发出正式提交的commit 请求

    • 收到协调器的commit请求后,参与者正式执行事务提交操作,并释放整个事务期间占用的资源

    • 参与者完成事务提交后,项协调器发送ACK消息

    • 协调器收到所有参与者节点反馈的ACK消息后,完成事务

  2. 事务回滚: 如果任意一个参与者节点在第一阶段返回的消息为终止,或者协调器节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息,那么这个事务会被回滚

    • 协调器项所有参与者发出rollback回滚操作的请求

    • 参与者利用准备阶段写入的Undo信息执行回滚,并释放在整个事务期间占用的资源

    • 参与者在完成事务回滚之后,项协调器发送回滚完成的ACK消息

    • 协调器收到所有参与者反馈的ACK消息后,取消事务

2.3 2PC的缺点

二阶段提交确实能够提供原子型的操作,但不幸的是,二阶段提交还是有几个缺点的:

  1. 性能问题:执行过程中,所有参与节点都是事务阻塞的,当参与者占用公共资源时,其他第三方节点访问公共资源就不能不处于阻塞状态,为了数据一致性而牺牲了可用性,对性能影响较大,不适合高并发性能场景。
  1. 可靠性问题: 2PC非常依赖协调者,当协调者发生故障时,尤其是第二阶段,那么所有的参与者就会处于锁定事务资源的状态中,而无法继续完成事务操作(如果协调者挂掉,可以重新选择一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞的问题)
  1. 数据一致性问题:在第二阶段中,当协调者项参与者发送commit请求后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这会导致只有一部分参与者接收到了commit请求,而在这部分参与者接到commit请求后就会执行commit操作,但是其他的参与者未接收到commit请求,于是整个分布式系统出现了数据一致性现象
  1. 二阶段无法解决的问题:协调者在发出commit消息之后宕机,而唯一接收到消息的参与者同时也发生宕机,那么即使协调者通过选择协议产生新的协调者,这条事务的状态也是不确定的,没有人知道事务是否被提交

3. 3PC

3PC三阶段提交协议,是二阶段提交协议的改进版本,三阶段提交有两个改动点:

  • 在协调者和参与者都引入超时机制

  • 在第一阶段和第二阶段中插入一个准备阶段,保证了在最后提交阶段之前各参与节点的状态时一致的

举个栗子:上课考试前,老师开始点名,点到的同学都会应一声”到”(CanCommit 阶段),老师开始发放试卷,同学们开始答题,答题完了之后举手表示已经做完等待老师收卷子(PreCommit阶段),下课后,老师说统一交卷,同学们听到后,陆续把试卷交给老师,并收拾好文具(doCommit阶段)

3PC会分为3个阶段:CanCommit准备阶段、PreCommit预提交阶段、DoCommit提交阶段,处理流程如下:

3.1 CanCommit 准备阶段

协调者项参与者发送CanCommit请求,参与者如果可以提交就返回YES反之则返回NO,具体流程如下:

  • 事务询问:协调者项所有参与者发送包含事务内容的CanCommit请求,询问是否可以提交事务,并等待所有参与者答复

  • 响应反馈:参与者收到CanCommit请求后,入股认为可以执行事务操作,则反馈Yes并进入预备状态,否则反馈NO

3.2 PreCommit 阶段

协调者根据参与者的反馈情况来决定是否可以进行事务的PreCommit操作。根据响应情况,有以下两种可能:

  1. 执行事务:假如所有参与者都反馈YES,协调者预执行事务,具体如下:

    • 发送预提交请求:协调者向参与者发送PreCommit请求,并进入准备阶段
    • 事务预提交:参与者收到PreCommit请求后,会执行本地事务操作,并将Undo和Redo信息记录到事务日志中(但不提交事务)
    • 响应反馈:如果参与者成功的执行了事务操作,则返回ACK响应,同时等待最终命令

  1. 中断事务:假如有任何一个参与者项协调者发送了NO响应,或者等待超时之后,协调者都没有接收到参与者的响应,那么就执行事务的中断,流程如下:

    • 发送中断请求:协调者向所有参与者发送abort请求
    • 中断事务:参与者接收到协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断

3.3 DoCommit 阶段

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

  1. 提交事务

    • 发送提交请求:协调者收到所有参与者发送的ACK响应,从预提交状态进入提交状态,冰箱所有参与者发送doCommit请求

    • 本地事务提交:参与者接收到doCommit请求之后,执行正式的事务提交,并在完成事务提交之后释放所有资源

    • 响应反馈:事务提交完成之后,向协调者发送ack响应

    • 完成事务:协调者接收到所有参与者的ack响应之后,完成事务

  1. 中断事务: 任何一个参与者反馈NO,或者等待超时后协调者无法收到所有参与者的反馈,即中断事务

    • 发送中断请求:如果协调者处于工作状态,向所有参与者发出abort请求

    • 事务回滚:参与者接收到abort请求之后,利用器在阶段二记录的Undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的资源

    • 反馈结果:参与者完成事务回滚之后,向协调者反馈ACK消息

    • 中断事务:协调者接收到参与者反馈的ACK消息之后,执行事务的中断

进入doCommit阶段后,无论协调者出现问题,或者协调者与参与者之间的网络出现问题,都会导致参与者无法接收到协调者发出的doCommit请求或abort请求。此时,参与者都会在等待超时之后,继续执行事务挑。

这其实基于概率来决定的,当进入第三阶段时,说明第一阶段收到所有参与者的CanCommit响应都是YES,意味着大家都同意了,并且第二阶段所有的参与者对协调者的PreCommit请求也是同意的。所以,当进入第三阶段时,由于网络超时等原因,虽然参与者没有收到commit或者abort请求,但是参与者相信成功提交的概率更大

3.3 3PC的优缺点

与2PC相比,3PC降低了阻塞范围,并且在等待超时后,协调者或参与者会中断事务,避免了协调者单点问题,阶段三中协调者出现问题时,参与者会继续提交事务。

数据不一致问题依然存在,当在参与者收到preCommit请求后等待doCommit指令时,此时如果协调者请求中断事务,而协调者因为网络问题无法与参与者正常通信,会导致参与者继续提交事务,造成事务不一致

2PC和3PC都无法保证数据绝对的一致性,一般为了预防这种问题,可以添加一个报警,比如监控到事务异常的时候,通过脚本自动补偿差异的信息

4. TCC

4.1 什么是TCC

TCC(Try Confirm Cancel) 是应用层的两阶段提交,所以对代码的侵入性很强,其核心思想是:针对每个操作,都要实现对应的确认和补偿操作,也就是业务逻辑的每个分支都要实现tryconfirmcancel三个操作。第一阶段是由业务代码编排来调用Try接口进行资源预留,当所有参与者的Try接口都成功了,事务协调者提交事务,并调用参与者的confirm接口真正提交业务操作,否则调用每个参与者的cancel接口回滚事务,并且由于confirm或者cancel有可能会重试,因此对应的部分需要支持幂等。

4.2 TCC的执行流程

TCC的执行流程可以分为两个阶段,分别如下:

  1. 第一阶段 Try:业务系统做检测并预留资源(枷锁、锁住资源),比如常见的下单,在try阶段,我们不是真正的减库存,而是把下单的库锁定住

  2. 第二阶段: 根据第一阶段的结果决定执行的是confirm还是cancel

    • confirm: 执行真正的业务(执行业务,释放锁)

    • cancel: 是对Try阶段预留的志愿的释放(出问题,释放锁)

4.3 TCC如何保证最终一致性

  • TCC事务机制是以Try为中心的,confirm确认操作和cancel取消操作都是围绕Try而展开,因此,Try阶段中的操作,其保障性是最好的,即使失败,仍有cancel取消操作可将其执行结果撤销
  • Try阶段执行成功并开始执行confirm阶段时,默认confirm阶段时不会出错的,也就是说只要Try成功,confirm一定成功(TCC涉及指出的定义)
  • confirm与cancel如果失败,由TCC框架执行重试补偿
  • 存在极低概率在CC环节彻底失败,则需要定时任务或人工介入

4.4 TCC的注意事项

  1. 允许空回滚

    空回滚出现的原因时Try超时或者丢包,导致TCC分布式事务二阶段的回滚,触发cancel操作,此时事务参与者未收到Try,但是却收到了cancel请求,如下图所示:

    所以cancel接口在实现时需要允许空回滚,也就是cancel执行时如果没有发现对应的事务xid或主键时,需要返回回滚成功,让事务管理器认为已经回滚

  1. 防悬挂控制:

    悬挂指的是二阶段的cancel比一阶段Try操作先执行,出现改为难题的原因是Try由于网络拥堵而超时,倒是事务管理器生成回滚,触发cancel接口,但之后拥堵在网络的Try操作又被资源管器收到了,但是cancel比Try先到,按照前面允许空回滚的逻辑,回滚会返回成功,事务管理器认为事务已经回滚成功,所以此时应该拒绝执行空回滚之后到来的Try操作,否则会产生数据不一致。因此我们可以在cancel空回滚之前,先记录改事务xid或业务主键,标识这条数据已经回滚过,Try接口执行前先检查这条事务xid或业务主键是否已经标记为回滚成功,如果时则不执行Try的业务操作

  1. 幂等控制由于网络原因或则会重试操作都有可能导致Try-Confirm-Cancel3个操作的重复执行,所以使用TCC时需要注意这三个操作的幂等控制,通常我们可以使用事务xid或者业务主键判重来控制

4.5 TCC方案的优缺点

  1. TCC事务机制相比上面介绍的XA事务机制,有以下优点

    • 性能提升:具体业务来实现,控制资源锁的粒度变小,不会锁定整个资源

    • 数据最终一致性:基于confirm和cancel的幂等行,保证事务最终完成确认或者取消,保证数据的一致性

    • 可靠性;解决了XA协议的协调者单点故障问题,由主业务发起并控制整个业务活动,业务活动管理器也变成多点,引入集群

  2. 缺点:

    • TCC的Try、confirm和cancel操作功能要按具体业务来实现,业务耦合度较高,提高了开发成本

5. Saga事务

5.1 什么是Saga事务

Saga事务核心思想是将长事务拆分为多个本地短事务并一次正常提交,如果所有短事务均执行成功,那么分布式事务提交;如果出现某个参与者执行本地事务失败,则有Saga事务协调根据相反顺序调用补偿操作,回滚已提交的参与者,使分布式事务回到最初的状态。 Saga事务基本协议如下:

  1. 每个Saga事务由一系列幂等的有序子事务(sub-transaction)Ti组成

  2. 每个Ti都有对应的幂等补偿动作Ci,补偿动作用于撤销Ti造成的结果

与TCC事务补偿机制相比,TCC由一个预留Try动作,相当于先保存一个草稿,然后在提交;Saga事务没有预留动作,直接提交

5.2 Saga的恢复策略

对于事务一场,Saga提供了两种恢复策略,分别如下:

  1. 向后恢复(backward recovery)

    当执行事务失败时,补偿所有已完成的事务,是”一退到底”的方式,这种话做法的效果是撤销之前所有成功的子事务,是的整个Saga的执行结果撤销,如下图:

    从上图可知事务执行到支付事务T3,但是失败了,因此事务回滚需要从C3、C2、C1依次进行回滚补偿,对应的执行顺序为:T1->T2->T3->C3->C2->C1

  1. 向前恢复(forward recovery)

    对于执行不通过的事务,会尝试重试事务,这里有一个假设就是每个子事务最终都会成功,这种方式适用于必须要成功的场景,事务失败了重试,不需要补偿。流程如下:

5.3 Saga事务的实现方式

Saga事务有两种不同的实现方式,分别如下:

  • 命令协调 Order Orchestrator

    中央协调器(Orchestrator 简称OSO)以命令/回复的方式与每项服务进行通信,全权负责告诉每个参与者该做什么以及什么时候该做什么,整体流程如下图:

    1. 事务发起方的主业务逻辑请求OSO服务开启订单事务

    2. OSO向库存服务请求扣减库存,库存服务回复处理结果

    3. OSO向订单服务请求创建订单,订单服务回复处理结果

    4. OSO向支付服务请求支付,支付服务回复处理结果

    5. 主业务逻辑接收并处理OSO事务处理结果回复

中央协调器OSO必须实现知道整个事务所需的流程,如果有任何失败,它还负责通过向每个参与者发送命令来撤销之前的操作来协调分布式的回滚,基于中央协调器协调一切时,回滚要容易的多,因为协调器默认时执行正向流程,回滚时只要执行反向流程即可

  • 事件编排 Event Choreography 命令协调方式基于中央协调器实现,所以有单点风险,但是事件编排方式没有这种。事件编排的实现方式中,每个服务产生的事件并监听其他服务的事件来决定是否采取行动。 在事件编排中,第一个服务执行一个事务,然后发布一个事务,该事件被一个或多个服务进行监听,这些服务在执行本地事务并发布(或不发布)新的事件。当最好一个服务执行本地事务并且不发布任何事件时,意味着分布式事务结束,或者它发布的事件没有被任何Saga参与者监听到都意味着事务结束
    1. 事务发起方的主业务逻辑发布【开始订单事件】
    2. 库存服务监听【开始订单事件】,扣减库存,并发布【库存已扣减事件】
    3. 订单服务监听【库存已扣减事件】,创建订单,并发布【订单已创建事件】
    4. 支付服务监听【订单已创建事件】,进行支付,并发布【订单已支付事件】
    5. 主业务逻辑监听【订单已支付事件】并处理
    如果事务涉及2~4个步骤,则非常合适使用事件编排的方式,它是实现Saga模式的自然方式,很简单,不需要太多的代码来构建

5.4 Saga事务的优缺点

  1. 命令协调设计的
  • 优点:

    • 服务之间关系简单,避免服务件循环依赖,因为Saga协调器会调用Saga参与者,但参与者不会调用协调器

    • 程序开发简单,只需要执行命令/回复,降低参与者的复杂性。

    • 易维护扩展,在添加新步骤时,事务复杂性保持线性,回滚更容易管理,更容易实施和测试

  • 缺点:

    • 中央协调器处理逻辑容易变得庞大复杂,导致难以维护

    • 存在协调器单点故障风险

  1. 事件编排涉及的优缺点

    • 优点:

      • 避免中央协调器单点故障风险

      • 当涉及的步骤较少服务开发简单,容易实现

    • 缺点:

      • 服务之间存在循环依赖的风险

      • 当涉及的步骤较多,服务见关系混乱,难以追踪调测

由于Saga模型没有Prepare阶段,因此事务间不能保证个理性。当多个Saga事务操作同意资源时,就会产生更新丢失、脏数据读取等问题,这时需要在业务层控制并发,例如:在应用层面枷锁,或者应用层面预先冻结资源。

6. 本地消息表

6.1 什么是本地消息表

本地消息表的核心思路就是将分布式事务拆分成本地事务进行处理,在该方案中有两个角色:事务主动方或事务被动方。

  • 事务主动方:需要额外新建事务消息表,并在本地事务中完成业务处理和记录事务消息,并轮询事务消息表中的数据发送事务消息
  • 事务被动方:基于消息中间件消费事务消息表中的事务

这样就可以避免以下两种数据不一致性:

  • 业务处理成功、事务消息发送失败

  • 业务处理失败、事务消息发送成功

6.2 本地消息表的执行流程

  1. 事务主动方在同一个本地事务中处理业务和写消息表操作

  2. 事务主动方通过消息中间件,通知事务被动方处理事务消息。消息中间件可以基于Kafka、RocketMQ消息队列,事务主动方主动写消息到消息队列,事务消费方消息并处理消息队列中的消息

  3. 事务被动方通过消息中间件,通知事务主动方已处理的消息

  4. 事务主动方接受中间件的消息,更新消息表的状态为已处理

一些必要的容错处理如下:

  • 当1处理出错,由于还在事务主动方的本地事务中,直接回滚即可

  • 当2、3处理出错,由于事务主动方本地保存了消息,只需要轮询消息重新通过消息中间件发送,通知事务被动方重新读取消息处理业务即可

  • 如果是业务处理失败,事务被动方可以发消息给事务主动方回滚事务

  • 如果事务被动方已经消费了信息,事务主动方需要回滚事务的话,需要发消息通知事务的被动方进行回滚事务

6.3 本地消息表的优缺点

  1. 优点:

    • 从应用设计开发的角度实现了消息数据的可靠性,消息数据的可靠性不依赖消息中间件,弱化了对MQ中间件特性的依赖
    • 方案轻量、容易实现
  2. 缺点:

    • 与具体的业务场景绑定,耦合性强,不可公用
    • 消息数据与业务数据同库,占用业务系统资源
    • 业务系统在使用关系型数据库的情况下,消息服务性能会收到关系型数据库并发性能的局限

7. MQ事务消息

7.1 MQ事务消息的执行流程

基于MQ的分布式事务方案本质上是对本地消息表的封装,整体流程和本地消息表一致,以为不通的就是将本地消息存在了MQ内部,而不是业务数据库中,如下图:

由于本地消息存在MQ内部,那么MQ内部的处理尤为重要,下面主要基于RocketMQ4.3之后的版本介绍MQ的分布式事务方案

7.2 RocketMQ事务消息

在本地消息表方案中,保证事务主动方写业务数据和写消息数据的一致性是基于数据库事务,而RocketMQ的事务消息相对与普通MQ提供了2PC的提交接口,方案如下:

  1. 正常情况:

    在事务主动方服务正常,没有发生故障的情况下,发消息流程如下:

    • 发送方向MQ Server发送half消息
    • MQ Server将消息持久化成功之后,向发送方ack确认消息已经发送成功
    • 发送方开始执行本地事务
    • 发送方根据本地是事务执行结果向MQServer提交二次确认(commit或者rollback)
    • MQ Server如果收到的是commit操作,则将消息标记为可投递,MQ 订阅方最终将收到该half消息;若收到的是rollback操作,则删除half消息,订阅方不会收到该消息
  1. 异常情况:

    在断网或者应用重启等异常情况下,图中的步骤4提交的二次确认超时未到达MQServer,此时的处理逻辑如下:

    • MQ Server对该消息发起消息回查
    • 发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果
    • 发送方根据检查得到本地事务的最终状态再次提交二次确认
    • MQ Server基于commit/rollback 对消息进行投递或删除

7.3 MQ 事务消息的优缺点

  1. 优点:相比本地消息表方案,MQ事务方案优点是:

    • 消息数据独立存储,降低业务系统与消息系统的耦合性
    • 吞出量大于使用本地消息表方案
  1. 缺点:

    • 一次消息发送需要两次网络请求(half消息+commit/rollback消息)
    • 业务处理服务需要实现消息状态回查接口

8. 最大努力通知

最大努力通知也称为定期校对,是对MQ事务方案的进一步优化。它在事务主动方增加消息校对的接口,如果事务被动方没有接受到主动方发送的消息,此时可以调用事务主动方提供的消息校对的接口主动会去。

在可靠消息事务中,事务主动方需要将消息发送除去,并且让接收方成功接收消息,这种可靠性发送是由事务主动方保证的;但是最大努力通知,事务主动方仅仅是最大努力(重试、轮询…)将事务发送给事务接收方,所以存在事务被动方接收不到消息的情况,此时需要事务被动方主动调用事务主动方的消息校对接口查询业务消息并消费,这种通知的可靠性是由事务被动方保证的。

所以最大努力通知适用于业务通知类型,例如微信交易的结果,就是通过最大努力通知方式通知各个商户,既有回调通知也有交易查询接口

9. 总结

  • 2PC/3PC: 依赖于数据库,能够很好的提供强一致性和强事务性,但延迟较高,比较适合传统的单体应用,在同一个方法中存在多次查询数据库操作的情况,不适合高并发和高性能要求的场景
  • TCC: 适用于执行事件确定且较短,实时性要求高,对数据一致性要求高,比如互联网金融企业最和行的三个交易服务:交易、支付、账务
  • Saga事务:由于saga事务不能保证隔离性,需要在业务层控制并发,适用于业务场景事务并发操作同一资源较少的情况,Saga由于缺少预提交动作,导致补偿动作的实现比较麻烦,例如业务是发送短信,补偿动作是在发送一次短信说明撤销,用户体验较差,所以Saga事务比较适用于补偿动作容易处理的场景
  • 本地消息表/MQ事务: 适用于事务中参与方支持操作幂等,对一致性要求不高,业务上能容忍数据不一致到一个人工检查周期,事务涉及的参与方、参与环节较少,业务上由队长/校验系统兜底