分布式事务2

0、写在最前面

目前大家开发时面临的大多数场景都是分布式的,而如何解决分布式事务一直分布式场景下一个讨论的话题,许多公司也有开源产品,本文希望通过介绍常用的分布式事务解决方案和实现细节,帮助读者了解相关知识。

1、分布式事务的由来

1.1、什么是事务?

事务(Transaction)通常的定义是:作为单个逻辑单元执行的一组操作,要么全部执行,要么全部不执行。它有四个明显的特征:原子性(Atomicity)、一致性(Correspondence)、隔离性(Isolation)、持久性(Durability),简称ACID【详情见附录】。结合一个简单的例子进行说明:一次用户的操作,涉及到多个不同数据表的数据变更,想要在一次操作内实现要么全部变更完成返回成功,要么全部不变更直接返回失败,如果执行到中途失败了需要将已执行的变更进行撤回,这就是一次典型的事务

分布式事务(Distributed Transaction)最早指的就是涉及多个资源的数据库事务,不过近年来随着SOA、微服务等概念的流行,分布式事务概念也有所泛化,根据上下文的不同分为系统事务(System Transaction)和业务事务(Business Transaction),前者多指数据库事务,而后者多指业务交易,需要关注的是如何保证交易的整体原子性和一致性,特点是时间跨度很长,比如餐厅线上预订,可能从用户点击预定到商户实际确认要一两天才能完成。不过无论哪一种事务,因为事务的持久性特征导致事务实现最终都需要依赖存储介质,因此在谈到事务时很难脱离数据库。

1.2、为什么会产生分布式事务?

产生分布式事务的原因大致可以分为两类,下面分别进行介绍。

1.2.1、服务的多节点

在SOA、微服务场景下,许多一开始大而全的服务都会业务领域进行拆分,这往往会导致原本在一个服务内就可以闭环完成的简单操作变成需要多个不同服务协作完成的复杂操作,而不同服务往往又会对应不同的数据资源,此场景下如果对操作有事务要求自然就产生了分布式事务的问题。

​​

服务多节点图

如上图,一个应用被拆分为了服务A(RemoteServiceA)、服务B(RemoteServiceB)、服务C(RemoteServiceB),分别对应了数据库A、数据库B、数据库C。如下示例,如果服务B某个方法(updateAll)既需要更新本地数据还需要通过调用服务A和服务B提供的服务更新各自数据库数据时,方法如何保证事务性就成了一个分布式事务问题,我们没法简单的通过配置常用的事务管理器管理这种场景下的整体事务,因为传统的事务管理器控制域都是作用在本地的,不支持跨服务传递。

代码块
Java
 
 
 
 
 
// 服务B
public class RemoteServiceB {

  public void updateAll() {
    // 更新本地数据库B的数据
      this.updateSthInDatabaseB();
    // 调用远程服务A的方法,更新数据库A的数据
      remoteServiceA.updateSthInDatabaseA();
    // 调用远程服务C的方法,更新数据库C的数据
      remoteServiceB.updateSthInDatabaseC();
  }

  private void updateSthInDatabaseA() {
  }
}
 

1.2.2、资源的多节点

当业务规模发展到一定程度,除了从业务领域角度进行纵向拆分外,往往还会考虑到数据的水平拆分,会把一些量大且重要的库按水平进行分表,而考虑到容灾和性能问题,还会进行分库分集群,这会导致原本在单个数据库内的简单数据操作变成了涉及多个数据库的跨库操作,此场景下如果对操作有事务要求自然就产生了分布式事务的问题。

​​

多资源节点图

如上图,一个服务(LocalService)对应数据被分散存储到三个不同数据库A、数据库B、数据库C。如下示例,如果服务中某个方法(updateAll)既需要同时更新三个数据库的数据时,方法如何保证事务性就成了一个分布式事务问题,我们没法简单的通过配置常用的事务管理器管理这种场景下的整体事务,因为常用的事务管理器控制域都是作用在本地的,不支持跨数据库传递。

代码块
Java
 
 
 
 
 
// 服务B
public class LocalService {

  public void updateAll() {
    // 更新数据库A的数据
      this.updateSthInDatabaseA();
    // 更新数据库B的数据
      this.updateSthInDatabaseB();
    // 更新数据库C的数据
      this.updateSthInDatabaseC();
  }

  private void updateSthInDatabaseA() {
    // 执行sql
  }

  private void updateSthInDatabaseB() {
    // 执行sql
  }

  private void updateSthInDatabaseC() {
    // 执行sql
  }
}
 

1.3、有哪些理论和问题?

一般来说分布式系统都脱离不开CAP理论【详情见附录】,该理论认为一个分布式系统中, 一致性(Consistency)、可用性(Availability)、分区容错性(Partition Tolerance),三者不可得兼。因此在设计分布式系统时,就存在一定的取舍,那么在解决分布式事务时,如果完全强调事务的ACID特征,按照理论就必然需要牺牲系统的可用性,但是可用性又是现在大型分布式系统的核心关注指标,这种矛盾就是分布式事目前面临的首要问题。

 

​​

CAP理论和BASE理论关系图

基于这样的问题背景,BASE理论【详情见附录】后来被提出,其强调基本可用(Basically Available)、软状态(Soft State)和最终一致性(Eventually Consistent)三个特点。该理论是对CAP中一致性和可用性权衡的结果,其核心思想是即使系统无法做到数据的实时强一致性(Strong Consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统在经过一段时候后达到最终一致性,以此提供系统整体的可用性。这种理论一定程度上调和了事务在分布式场景下所面临的一致性与可用性之间的矛盾,也正因此涌现了许多基于最终一致性的分布式事务解决方案,下面会着重介绍几个常见的经典解决方案。

2、常见解决方案

2.1、你需要先了解这些概念

名词

概念

事务管理器(Transaction Manager)

事务管理器是一个独立的服务,用于控制分布式事务的生命周期,包括创建主事务记录、分支事务记录,并根据分布式事务的状态,调用参与者提交或回滚方法。

资源管理器(Resource Manager)

任意类型的持久化数据存储,如常见的关系型数据库甚至消息队列。

通常情况下,我们因为不直接操作存储资源,所以可以理解为数据源适配器。

事务协调器(Transaction Coordinator)

事务协调器是一个独立的模块,用来控制事务的提交和回滚操作。

事务协调器可嵌入在客户端中独立运行,也可部署在Server端作为独立服务。

全局事务(Global Transaction)

全局事务是一个逻辑上的概念,他并不是具体执行的某段逻辑。

一个全局事务包含了多个具体的分支事务,发起方通过修改全局事务的状态,控制分支事物的提交/回滚。

分支事务(Branch Transaction)

分支事务是一个逻辑上的概念,一个分布式事务可能包含多个数据库本地事务,在分布式事务框架下,分支事务可能是一个分库上执行的 SQL 语句,或是一个自定义模式服务的调用。

事务发起方(Launcher)

分布式事务的发起方负责启动分布式事务,通过调用参与者的服务,将参与者纳入到分布式事务当中,并决定整个分布式事务是提交还是回滚。一个分布式事务有且只能有一个发起方。

事务参与方(Participant)

参与者提供分支事务服务。当一个参与者被发起方调用,则被纳入到该发起方启动的分布式事务中,成为该分布式事务的一个分支事务。一个分布式事务可以有多个参与者。

2.2、两阶段提交

两阶段提交(Two-Phase Commit,以下简称2PC)方案,通常也被称为一种协议,从字面意思理解就是事务分两个阶段进行提交,实际方案确实如此,整个方案在执行分布式事务的过程中被分为两个重要阶段:

  • 一阶段——准备阶段(Prepare Phase):事务发起方通过事务管理器发起一个分布式事务后,由事务协调器(事务管理器)通知各个事务参与方(资源管理器),给他们发送一条prepare指令,每个参与者要么返回失败,要么就进行执行本地事务(分支事务),写本地redo和undo日志,但是不进行事务的commit,到达一种临界状态,此时每个参与者持有当前本地事务涉及的锁资源不释放;

  • 二阶段——提交阶段(Commit Phase):事务协调器收集所有事务参与方在一阶段返回的信息,如果都成功则通知所有参与方进行事务提交(Commit),如果有返回失败或超时,则通知所有参与方进行事务回滚(Rollback)。参与方根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。

如下图是2PC方案的简单流程示意图,不难发现事务管理器直接与资源管理器进行交互,中间不需要业务逻辑的介入。

​​

2PC方案流程示意图

 

2.2.1、优点

  • 数据库层面(资源管理器)层面控制分布式事务,业务层面无感知或低侵入

  • 强一致性

2.2.2、缺陷

  • 同步阻塞:如上图,整个执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。

  • 单点故障:由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)

  • 数据不一致:在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据部一致性的现象。

  • 二阶段无法解决的问题:协调者发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。

举例:下单支付过程中,如果在支付中引发异常,回滚可恢复一致的状态,账户金额不会有变化;如果在支付成功后,因为业务原因不能正常履约,此时需要做退款,这就叫补偿;

2、Cancel和Compensate从语义角度是撤消了事务的行为,但未必能将数据完全返回到执行事务前的状态。(例如,如果事务触发消息推送, 则可能无法撤消此操作,但是可以通过再发送一个消息用于补偿)

3、在分布式场景下,因为延时等网络问题,可能会导致同一个事务内,Cancel请求比Try请求先到达事务参与方,称为空回滚问题,此时如果返回成功,那么当Try后到达后将会导致资源一直被锁定无法释放,称为事务悬挂问题,针对这个问题实现,框架一般通过默认生成一个回滚记录来保证Try请求后进来也不会造成资源锁定

2.2.3、与XA的关系

XA协议【详情见附录】是X/OPEN 提出的分布式事务处理规范,采用两阶段提交方式来管理分布式事务。

2.3、TCC

Try-Confirm-Cancel(以下简称TCC)方案,和严格遵守ACID的2PC方案不同,主要实现的是最终一致性,且主要面向业务层面的事务控制,通过对业务逻辑(业务系统自己实现)的调度来实现分布式事务,整个方案有三段业务逻辑,分别为尝试操作(Try)、确认操作(Confirm)、取消操作(Cancel),按执行步骤分为两个阶段:

  • 一阶段——尝试操作(Try):事务发起方在业务代码中,显示的对所有参与事务的参与方提供的Try操作,进行业务逻辑前的业务检查(一致性),完成业务资源的预留和锁定(准隔离性);

  • 二阶段——确认操作(Confirm):如果一阶段执行成功,不做业务检查,执行业务逻辑,使用Try阶段预留的业务资源;

  • 二阶段——取消操作(Cancel):如果流程中执行失败,执行补偿逻辑,对Try阶段预留的业务资源进行释放;

还有一种常见的变种方案,方案中有两段业务逻辑,分别为执行操作(Do)和补偿操作(Compensate),按执行步骤同样分为两个阶段:

  • 一阶段——执行操作(Do):直接执行业务校验和业务逻辑,并对结果持久化,业务操作结果外部可见;

  • 二阶段——补偿操作(Compensate):逆向逻辑,一阶段操作失败后的抵消和补偿,如果一阶段成功则不需要执行二阶段;

两种方案的区别在于业务逻辑的粒度,Do操作相当于Try + Confirm,如果Try出现失败的情况很多则建议使用标准的TCC实现,如果Try很少出现失败则可以使用变种方案

如下图是TCC方案的简单流程示意图,可以发现事务参与方的Try方法是有发起方在业务代码中进行调用的,而事务参与方的Confirm和Cancel方法的调用则是由事务协调器来调用的。

​​

TCC方案流程示意图

2、Cancel和Compensate从语义角度是撤消了事务的行为,但未必能将数据完全返回到执行事务前的状态。(例如,如果事务触发消息推送, 则可能无法撤消此操作,但是可以通过再发送一个消息用于补偿)

3、在分布式场景下,因为延时等网络问题,可能会导致同一个事务内,Cancel请求比Try请求先到达事务参与方,称为空回滚问题,此时如果返回成功,那么当Try后到达后将会导致资源一直被锁定无法释放,称为事务悬挂问题,针对这个问题实现,框架一般通过默认生成一个回滚记录来保证Try请求后进来也不会造成资源锁定

2.3.1、优点

  • 保证隔离性

  • 方案灵活,基本能覆盖所有业务场景,可操作性强

  • 整体可用性高

2.3.2、缺陷

  • 成本高,如上图不难发现所有事务参与方都需要实现Try、Confirm、Cancel三个逻辑上的方法

  • 代码侵入高,原因同上

 

2.4、Saga

Saga是一种常见的处理长活事务(Long Lived Transaction)的方案,其核心思想是通过将一个跨服务跨数据库的业务流程(全局事务),分解为一些列可以保证事务性的业务动作(分支事务)集合(这种思想有点类似于分治思想),每个业务动作(Do)都是可以保持ACID特性的真实事务,且都需要提供相应的补偿操作(Compensate),按照一定顺序去执行这些动作,如果所有动作都执行成功则认为整体业务流程完成(事务提交),如果中间发生异常情况则需要通过补偿操作进行补偿。在Saga中,按照执行顺序分为两种流程:

  • 正向业务流程:按照一定顺序依次执行所有的业务动作(Do),每个业务动作都在执行完立即提交;

  • 逆向补偿流程:如果业务流程执行出现异常,则通过补偿操作(Compensate),按照业务流程的反向,依次撤销和补偿已经执行过的业务操作;

如下图是Saga方案的简单流程示意图,绿色即为正向流程,橙色是反向流程。

​​

Sage方案流程示意图

 

 

2.4.1、优点

  • 适合长流程状态驱动的场景

  • 方案灵活

2.4.2、缺陷

  • 隔离性差,如图所示一旦事务A执行完即提交,此时整个全局事务未完成时,外部已经可以获取事务A提交的数据,容易造成脏读

  • 还原度差,因为隔离性差,所以一旦需要走逆向补偿流程时,完全还原到提交前的状态在高并发时基本不太可能

2.5、可靠消息

对于常见的微服务系统,系统间的调用大部分场景都是通过RPC或HTTP进行同步调用,此时通过上面提到的2PC、TCC、Saga等方案来实现分布式事务是较为合适的,但是如果系统间不直接依赖,而是通过MQ等方式进行异步交互时,应该如何实现分布式事务呢?这个时候,就要可靠消息方案来解决了。

可靠消息方案主要指的是在业务互动的主动方与被通知方之间,架设一个稳定可靠的消息系统来保证业务活动主动方执行本地业务动作与通知具备事务性,也就是说如果业务活动的主动方事务执行成功,被动方一定能收到相应的业务通知,反之如果主动方的事务回滚则业务通知不会发送给被动方。

整个方案分为两个步骤:

  1. 投递消息:

    1. 在业务活动的主动方提交事务前向可靠消息系统发送消息,可靠消息系统接收后暂存消息并不进行投递;

    2. 在业务活动的主动方提交事务后向可靠消息系统确认发送消息,可靠消息系统进行消息的投递,通过重试等手段保证消息投递MQ成功;

    3. 在业务活动的主动方事务回滚后向可靠消息系统删除消息,可靠消息系统对消息进行清理,不进行投递;

  2. 接收消息:

    1. 监听并处理对应MQ主题产生的消息,如果接收并处理成功则ACK给MQ,如果处理失败可以通过死信进行多次重试

如下图是可靠消息方案的简单流程示意图,其中上游系统对应的是活动通知方,而下游系统对应的是被通知方。

​​

可靠消息方案示意图

2、通过mq来进行系统交互,存在的一个比较大的问题就是需要保证消息的有序性,尤其是对kafka这种需要通过业务保证有序性时,需要做好消息的隔离

2.5.1、优点

  • 可用性强,可靠消息服务独立部署,可伸缩

  • 被通知方无改造成本

2.5.2、缺陷

  • 可靠消息服务实现成本高,有技术难度

  • 最终一致性的实时性无法保障

  • 一次事务,需要至少发送两次消息

2.6、最大努力通知

最大努力通知服务是一种较简单也是现实中常用的实现最终一致性的分布式事务解决方案,整个方案分为两个步骤:

  1. 投递消息:业务活动的主动方,在完成业务处理之后,向业务活动的被动方发送消息,允许消息丢失;

  2. 消息补偿:业务活动的被动方,通过一定的策略订单向业务活动主动发发起查询,恢复丢失的业务消息;

如下图是最大努力通知方案的简单流程示意图,注意其中下游系统对应的是活动通知方,而上游系统对应的是被通知方。

​​

最大努力通知方案流程示意图

  • 可靠消息系统:关注业务活动的主动方在发消息过程的事务性,业务活动的主动方需要为可靠消息系统提供查询补偿操作

  • 最大努力通知:关注业务活动的被动方是否收到消息,业务活动的主动方需要为被动方提供查询补偿操作

2.6.1、优点

  • 可用性强

  • 支持异步场景

  • 方案简单

2.6.2、缺陷

  • 活动通知方需要提供查询能力,活动被通知放需要实现补偿

  • 下游依赖上游

  • 最终一致性的实时性差

2.7、需要注意的点

分布式事务下,虽然方案可能选择不同,但多少都会涉及到流程重试、业务补偿以及业务隔离,这就要求在设计开发相关业务操作(如TCC中的Try、Confirm、Cancel三个操作)时需要考虑到操作的可查询幂等

2.7.1、可查询

在分布式事务下,事务当前的业务状态和业务数据应该是可以查询的,因此这就要求一次业务操作应该具备一个全局唯一的标识,便于查询和补偿,如:

  • 业务的单据凭证号(订单号)

  • 系统分配的流水号

  • 对操作资源的组合(商户编号 + 用户编号)

2.7.2、幂等

幂等是一个老生常谈的问题,不是只有分布式事务场景下才需要考虑,主要指的是针对一个操作,任意多次执行产生的业务影响和一次执行产生的业务影响相同,一般来说实现上有两种思路:

  • 通过业务逻辑实现幂等性(如创建订单,如果对应订单之前已经创建成功则返回创建成功的订单信息,而不应该返回创建失败)

  • 存储所有请求和结果,检测到重复请求后,直接返回之前处理的结果

 

2.8、对比

下面对上面几种方案进行简单对比,可以发现没有适用于所有场景的完美方案,都有各自的优缺点,通常情况下同步场景使用TCC的较多,而异步场景使用最大努力通知的较多。

方案

一致性

隔离性

并发

业务成本

技术成本

灵活度

代码侵入

开源项目

适用场景

2PC(XA协议)

强一致性

需要设置数据隔离级别为SERIALIZABLE,将导致衰退严重

无,数据库原生支持

几乎无

Seata AT

不推荐

TCC

最终一致性

Try保障隔离

高并发

Seata TCC

EasyTransaction

ByteTCC

Swan

各种同步场景

Saga

最终一致性

高并发,状态驱动

中低,需要提供逆向方法

Seata Saga

流程节点多且长

可靠消息

最终一致性

高并发,但整体性能取决于可靠消息系统,一般会有限流保障

低,实现事务状态反查

高,可靠消息系统的研发

RocketMQ

Qmq

Mafka

下游结果不影响上游执行的异步流程

最大努力通知

最终一致性

高并发,基本不影响整体性能

低,通知方提供查询补偿能力

基本无

 

业务执行结果的通知

3、框架实现细节

上面探讨了几种常见的分布式事务方案,下面结合代码介绍两种框架的实现细节

3.1、TXC框架

TXC全称是(Tabao Transaction Constructor),由阿里2014年启动的项目,以满足应用程序架构从单一服务变为微服务所导致的分布式事务问题,2016年更名为GTS,2019年阿里开源了其分布式事务产品Seata【详情见附录】,TXC在其中被当做一种支持模式为用户提供分布式事务能力,简称AT模式。在TXC中,开发只需要关注业务逻辑的实现,而不需要关注分布式场景下的事务保证,TXC会通过框架代理的方式完成事务的注册、提交、回滚等操作,具有代码无侵入,接入简单,适用场景广泛等优点

TXC其实是2PC方案的应用层实现,因此仍然保留了2PC的一阶段准备和二阶段提交的思想,不同点在于将数据库层面的实现放到应用层框架中实现,原理是在应用层实现了SQL解析并自动补偿,在TXC中,分布式事务的执行流程如下图左边所示:

  1. 发起方通过事务管理器开启分布式事务(所以事务参与方通过各自的资源管理器向事务协调器注册全局事务记录);

  2. 执行业务流程,各个事务参与方执行各自的分支事务(资源管理器想事务协调器上报状态);

  3. 发起方通过事务管理器结束分布式事务,事务一阶段结束(事务管理器通知事务协调器提交/回滚分布式事务);

  4. 事务协调器汇总事务信息,决定分布式事务是提交还是回滚;

  5. 事务协调器通知所有事务参与方进行提交或回滚,事务二阶段结束;

  • XA在数据库层面进行了分布式事务的支持

  • TXC在应用层面通过代理SQL的方式进行了分布式事务的支持

​​

TXC框架及流程示意图

下面对TXC在一阶段业务逻辑执行、二阶段提交和二阶段回滚中所作工作做详细的介绍:

3.1.1、一阶段执行

如上图右边所示,在一阶段执行过程,TXC 会拦截“业务 SQL”,首先解析 SQL 语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,然后执行“业务 SQL”更新业务数据,在业务数据更新之后,再将其保存成“after image”,最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。核心代码如下:

代码块
Java
 
 
 
 
// 代理sql执行
protected T executeAutoCommitFalse(Object[] args) throws Exception {
  // 构建before image
  TableRecords beforeImage = beforeImage();
  // 执行业务SQL
  T result = statementCallback.execute(statementProxy.getTargetStatement(), args);
  // 构建after image
  TableRecords afterImage = afterImage(beforeImage);
  // 通过before image 和 after image 生成undolog
  prepareUndoLog(beforeImage, afterImage);
  // 返回业务sql执行结果
  return result;
}
 

结合一个具体SQL来看,以UPDATE为例,假设当前事务参与方需要执行如下SQL:

代码块
SQL
 
 
 
 
update table_1 set biz_status` = 10 where id = 10 and biz_status = 0;
 

在实际执行时有如下几步动作:

  1. 框架通过代理statement获取到业务SQL,并进行解析,知道SQL为UPDATE类型,目标库名称是table_1,尝试获取table_1表的数据结构,

  2. 结合table_1表结构,以及业务SQL,生成一条对应的SELECT语句,查询到UPDATE对应条件的数据在变更前的原数据,以下是框架生成的SQL

    代码块
    SQL
     
     
     
     
    SELECT id, order_id, biz_status, add_by, add_time, update_by, update_time FROM table_1 WHERE id = 10 and biz_status = 0 FOR UPDATE;
     
  3. 执行业务SQL

    代码块
    SQL
     
     
     
     
    update table_1 set biz_status = 10 where id = 10 and biz_status = 0;
     
  4. 结合table_1表结构,以及业务SQL,生成一条对应的SELECT语句,查询到UPDATE对应条件的数据在变更后的新数据,以下是框架自动生成的SQL

    代码块
    SQL
     
     
     
     
    SELECT id, order_id, biz_status, add_by, add_time, update_by, update_time FROM table_1 WHERE id = 10;
     
  5. 生成行锁

    代码块
    Java
     
     
     
     
    table_1:10
     
  6. 生成一条undo log记录

    代码块
    Java
     
     
     
     
    public class SQLUndoLog {
        private SQLType sqlType;
        private String tableName;
        private TableRecords beforeImage;
        private TableRecords afterImage;
    }
     
  7. 提交本地事务并一同将undo log刷至数据库

    代码块
    Java
     
     
     
     
    // 事务提交
    // Step 1:判断context是否再全局事务中,如果在则进行提交,到Step2。
    // Step 2: 注册分支事务并加上全局锁,如果全局锁加锁失败则抛出异常。
    // Step 3: 如果context中有undolog,那么将Unlog刷至数据库。
    // Step 4: 提交本地事务。
    // Step 5:报告本地事务状态,如果出现异常则报告失败,如果没有问题则报告正常。
    public void commit() throws SQLException {
      if (context.inGlobalTransaction()) {
        processGlobalTransactionCommit();
      } else if (context.isGlobalLockRequire()) {
        processLocalCommitWithGlobalLocks();
      } else {
        targetConnection.commit();
      }
    }
    // 处理全局事务的commit
    private void processGlobalTransactionCommit() throws SQLException {
      try {
        register();
      } catch (TransactionException e) {
        recognizeLockKeyConflictException(e);
      }
      try {
        if (context.hasUndoLog()) {
          UndoLogManager.flushUndoLogs(this);
        }
        targetConnection.commit();
      } catch (Throwable ex) {
        report(false);
        if (ex instanceof SQLException) {
          throw new SQLException(ex);
        }
      }
      report(true);
      context.reset();
    }
     

3.1.2、二阶段提交

如上图右边所示,二阶段如果是提交的话,因为“业务 SQL”在一阶段已经提交至数据库, 所以TXC框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。关键代码如下:

代码块
Java
 
 
 
 
 
// 这里将我们的分支事务提交的信息,放到一个队列中,异步去处理,也就是异步删除我们的undolog数据,因为提交之后undolog数据没用了。
// 这里有人可能会问如果当你将这个信息异步提交到队列中的时候,机器宕机,那么就不会执行异步删除undolog的逻辑,那么这条undolog是不是就会成为永久的脏数据呢?
// 这里框架为了防止这种事出现,会定时扫描某些较老的undolog数据然后进行删除,不会污染我们的数据。
public BranchStatus branchCommit(BranchType branchType, String xid, long branchId, String resourceId, String applicationData) throws TransactionException {
        if (!ASYNC_COMMIT_BUFFER.offer(new Phase2Context(branchType, xid, branchId, resourceId, applicationData))) {
            LOGGER.warn("Async commit buffer is FULL. Rejected branch [" + branchId + "/" + xid + "] will be handled by housekeeping later.");
        }
        return BranchStatus.PhaseTwo_Committed;
}
 

3.1.3、二阶段回滚

如上图右边所示,二阶段如果是回滚的话,TXC框架就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。关键代码如下:

代码块
Java
 
 
 
 
 
// 这里会先获取到我们的数据源,接下来调用我们的重做日志管理器的undo方法进行日志重做,
// 其核心逻辑是查找到我们的undolog然后将里面的快照在我们数据库进行重做。
public BranchStatus branchRollback(BranchType branchType, String xid, long branchId, String resourceId, String applicationData) throws TransactionException {
  DataSourceProxy dataSourceProxy = get(resourceId);
  if (dataSourceProxy == null) {
    throw new ShouldNeverHappenException();
  }
  try {
    UndoLogManager.undo(dataSourceProxy, xid, branchId);
  } catch (TransactionException te) {
    if (te.getCode() == TransactionExceptionCode.BranchRollbackFailed_Unretriable) {
      return BranchStatus.PhaseTwo_RollbackFailed_Unretryable;
    } else {
      return BranchStatus.PhaseTwo_RollbackFailed_Retryable;
    }
  }
  return BranchStatus.PhaseTwo_Rollbacked;
}
 

1、TXC中before image类似mysql中的undo log,而after image类似mysql中的redo log概念,通过将数据库的相关能力在业务框架层实现,实现了业务无感知的分布式事务控制

2、TXC这种框架更适合跨库事务,偏向于数据层面,当事务中包含业务补偿动作时可能就无法支持,比如失败时需要给用户发一条失败信息等,这种情况可能还是需要使用TCC和Saga的方案更加合适

3.2、本地TCC框架

本地TCC主要指的是TCC框架的一种轻量化实现,公司的Swan框架、开源的EasyTransaction、ByteTCC等对其均有实现,其主要思想是事务的协调不再依赖远端 TC 集群,而是将 TC 的功能内嵌到客户端 SDK 中,随着业务机器部署,整个系统不存在单点风险,没有网络延时隐患,更加轻量级,可用性只依赖业务数据库的可用性。但是 LocalTCC 模式只能协调一层 TCC 事务,无法支持层级调用,适合于追求高性能和轻量化的用户使用。另外事务的二阶段的 Confirm(Cancel)也由事务发起方在代码里进行同步调用,不再通过 TC 进行控制。本地模式和远端模式的适用场景并不相同,可以互为补充,好处是涉及到跨端调用时不需要外部方进行项目改造,只需要事务发起方自己进行改造即可。

如下图左边所示,我们可以发现原来经典TCC方案中各个事务参与方都需要实现的Try、Confirm、Cancel操作已经不再需要,转而由事务发起方来实现,事务发起方在实现的Try、Confirm、Cancel方法内显示调用事务参与方对应的业务校验、业务执行、业务撤销等业务方法,完成业务逻辑上的分布式事务控制。

​​

本地TCC框架及流程示意图

下面我们以公司的Swan实现为例对框架在两个阶段三个操作中所作工作做详细的介绍:

3.2.1、一阶段Try

主要有三步操作:

1、通过AOP切片的方式,在申明需要分布式事务支持的业务方法执行前,创建事务上下文,关键代码如下:

代码块
Java
 
 
 
 
 
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
  BranchContext context = TccTransactionContextUtil.getBranchContext();
  if (context == null) {
    Class<?> targetClass = (methodInvocation.getThis() != null ? AopUtils.getTargetClass(methodInvocation.getThis()) : null);
    Method specificMethod = ClassUtils.getMostSpecificMethod(methodInvocation.getMethod(), targetClass);
    final Method method = BridgeMethodResolver.findBridgedMethod(specificMethod);
    SwanLocalTccTransactional annotation = getAnnotation(method, SwanLocalTccTransactional.class);
    if (annotation != null) {
      // 创建事务上下文,包含事务id生成、机器信息、事务配置等
      context = createTryContext(annotation, methodInvocation);
 

我们再来看下在事务管理器中执行本地TCC事务的具体逻辑,可以发现其主要工作是生成一条事务日志并持久化,之后进行业务逻辑的执行,如果执行失败抛出异常则自动进行二阶段cancel,如果执行完成则自动进行二阶段confirm,从try到confirm/cancel的过程业务不需要关注,由框架自动进行路由。

代码块
Java
 
 
 
 
 
 
@Override
public Object beginLocalTccTransaction(final MethodInvocation userMethodInvocation,
                                       final BranchContext context) throws Throwable {
  final LocalTccTransaction transaction = context.getLocalTccTransaction();
  final String domain = transaction.getDomain();
  final SwanResourceManager tccResourceManager = ResourceManagerRegistry.getInstance().getResourceManager(transaction.getDomain());
  return MetricsTemplate.executeWithCatTransaction(new MetricsTemplate.Executor() {
    @Override
    public Object execute() throws Throwable {
      Cat.logEvent("Swan.XID", transaction.getXid());
      try {
        // 插入一条事务日志
        tccResourceManager.getRepository().insertLocalTccTransaction(transaction);
      } catch (Throwable e) {
        // 插入数据库异常,这里不打点,统一到顶层打 REGISTER_ERROR
        // 外部补货到会进行异常打点
        throw new SwanTransactionException(transaction.getXid(), SwanTransactionException.Code.REGISTER_LOCAL_TX_FAILED, e);
      }
      Object result;
      try {
        // 这里会执行具体的业务逻辑
        result = transactionTemplate.execute(transaction.getXid(), userMethodInvocation);
      } catch (Throwable innerEx) {
        // 业务 try 方法原始异常打点,既打印本地日志也打印 CAT
        LOGGER.error("TRY_METHOD_[" + domain + "]_ERROR", innerEx);
        Cat.logErrorWithCategory("TRY_METHOD_[" + domain + "]_ERROR", innerEx);
        // 会 throw 包装好的 SwanTransactionException
        // 自动进入二阶段cancel
        cancelLocalTccTransaction(transaction, tccResourceManager, context, innerEx);
        throw innerEx;
      }
      // 一旦 commit 失败,会 throw 包装好的 SwanTransactionException
      // 外部补货到会进行异常打点
      commitLocalTccTransaction(transaction, tccResourceManager, context);
      return result;
    }
  }, String.format(TCC_LOCAL_CAT_TYPE, domain), TCC_LOCAL_REGISTER_CAT_NAME, transaction.getDomain(), LocalMetrics.LOCAL_REGISTER.getMetricsId());
}
 

3.2.2、二阶段Confirm和Cancel

下面我们再来看看二阶段confirm是如何实现的,Swan提供了配置化能力,允许配置是同步confirm还是异步confirm。

代码块
Java
 
 
 
 
 
private void commitLocalTccTransaction(final LocalTccTransaction transaction,
                                           final SwanResourceManager tccResourceManager,
                                           final BranchContext context) throws SwanTransactionException {
  // 设置事务状态为NEED_CONFIRM
  transaction.setStatus(GlobalStatus.NEED_CONFIRM);
  transaction.setOnePhaseResult(JSON.toJSONString(context.getOnePhaseResultContext()));
  transaction.setMtraceParams(JSON.toJSONString(context.getMtraceParams()));
  transaction.setUpdateTime(System.currentTimeMillis());
  final LocalTccDomainConfig domainConfig = tccResourceManager.getLocalTccDomainConfig();
  MetricsTemplate.executeWithoutCatTransaction(new MetricsTemplate.Executor() {
 

我们再来看同步confirm具体是如何实现的,不难发现这段代码被抽象为补偿操作(类似上文在TCC方案提到的Do-Compensate变种方案),实际cancel也会复用当前代码逻辑,其主要流程是更新事务状态,并调用业务申明的Confirm(Cancel)方法,如果操作成功则删除事务日志,最后清理掉补偿任务队列中所有关于当前事务的重试任务,具体代码如下:

代码块
Java
 
 
 
 
 
 
private void syncCompensate(final LocalTccTransaction transaction,
                                final SwanResourceManager tccResourceManager) throws Throwable {
        try {
            SwanTccCompensateTask.addRunningTask(transaction.getXid());
            //同步补偿时next_execute_time改成当前时间+1年,保证同步执行时异步补偿不会扫到这条事务
            //next_execute_time计算时使用ctime和utime+策略进行计算,next_execute_time本身仅0作为结束标志
            //如果同步补偿失败 next_execute_time 会被更新为实际下次执行时间, 如果更新失败静待回滚
            transaction.setNextExecuteTime(System.currentTimeMillis() + Constants.ONE_YEAR_TIME_MILLS);
          // 更新事务状态
            if (tccResourceManager.getRepository().updateLocalTccTransaction(transaction) != 1) {
                throw new SwanRepositoryException(String.format("cannot find transaction log when update, xid:%s, status:%s", transaction.getXid(), transaction.getStatus()));
            }
            Object[] arguments = (Object[]) tccResourceManager.getSerializer().deserialize(transaction.getParams().toByteArray(), tccResourceManager.getSwanTccMethodInvoker().getParameterTypes());
            // 一阶段成功,执行confirm操作
          if (GlobalStatus.NEED_CONFIRM == transaction.getStatus()) {
                Transaction t = Cat.newTransaction(String.format(TCC_LOCAL_CAT_TYPE, transaction.getDomain()), TCC_LOCAL_SYNC_CONFIRM_CAT_NAME);
                try {
                    transactionTemplate.executeSyncCompensate(GlobalStatus.NEED_CONFIRM, transaction.getXid(), tccResourceManager.getSwanTccMethodInvoker(), arguments);
                  //更新事务状态为CONFIRM_SUCCESS
                  transaction.setStatus(GlobalStatus.CONFIRM_SUCCESS);
                    //更新事务状态并删除
                  tccResourceManager.getRepository().deleteLocalTccTransaction(transaction);
                } catch (Throwable e) {
                    t.setStatus(e);
                    // 业务 confirm 方法原始异常打点
                    reactIfSyncCompensateFailed(transaction, tccResourceManager, e);
                } finally {
                    if (t != null) {
                        t.complete();
                    }
                }
            } else {//一阶段失败,执行cancel方法
                Transaction t = Cat.newTransaction(String.format(TCC_LOCAL_CAT_TYPE, transaction.getDomain()), TCC_LOCAL_SYNC_CANCEL_CAT_NAME);
                try {
                  //调用业务声明的cancel方法
                    transactionTemplate.executeSyncCompensate(GlobalStatus.NEED_CANCEL, transaction.getXid(), tccResourceManager.getSwanTccMethodInvoker(), arguments);
                    //更新事务状态为CANCEL_SUCCESS
                  transaction.setStatus(GlobalStatus.CANCEL_SUCCESS);
                  //更新事务状态并删除
                    tccResourceManager.getRepository().deleteLocalTccTransaction(transaction);
                } catch (Throwable e) {
                    t.setStatus(e);
                    // 业务 cancel 方法原始异常打点
                    reactIfSyncCompensateFailed(transaction, tccResourceManager, e);
                } finally {
                    if (t != null) {
                        t.complete();
                    }
                }
            }
        } finally {
          // 清理掉所有当前任务的重试任务
            SwanTccCompensateTask.removeRunningTask(transaction.getXid());
        }
    }
 

对于异步的confirm操作,框架则是通过抽象了一个补偿任务,定期执行补偿任务从数据库批量拉取事务日志来实现异步的confirm,关键代码如下:

代码块
Java
 
 
 
 
 
@Override
public void run() {
  try {
    checkAndSetMtraceTest();
    final int batchSize = 32;
    SortedMap<String, LocalTccTransaction> transactionsMap;
    while (true) {
      // 从数据库获取需要补偿任务
      try {
        transactionsMap = repository.findLocalTccNextExecuteTime(lastTransactionId, batchSize, globalStatus);
        if (transactionsMap == null || transactionsMap.isEmpty()) {
          lastTransactionId = "";
          return;
        }
      } catch (SwanRepositoryException e) {
        Cat.logErrorWithCategory("SWAN_RETRIEVE_NEXT_TX", e);
        LOGGER.error("retrieve next execute transaction log error", e);
        return;
      }
      // 提交补偿任务
      List<Future<?>> tasks = new ArrayList<>(transactionsMap.size());
      for (LocalTccTransaction transaction : transactionsMap.values()) {
        tasks.add(taskExecutor.submit(new SwanTccCompensateTask(transactionManager, isTest, serializer, transaction, swanTccMethodInvoker)));
        lastTransactionId = transaction.getXid();
      }
      // 如果最后一批 select 出的数目正好是 32,那么不会进入这个逻辑,
      // 需要依靠下一次 select empty 时重置 lastTransactionId
      if (transactionsMap.size() < batchSize) {
        for (; ; ) {
          int finishCount = 0;
          for (Future<?> future : tasks) {
            if (future.isDone() || future.isCancelled()) {
              finishCount += 1;
            }
          }
          //全部执行完了则开始下一轮
          if (finishCount == tasks.size()) {
            break;
          }
        }
        lastTransactionId = "";
        break;
      }
    }
  } catch (Throwable e) {
    Cat.logErrorWithCategory("SWAN_RETRIEVE_NEXT_TX", e);
    LOGGER.error("retrieve next execute transaction log, unexpected error", e);
  } finally {
    clearMtraceTest();
  }
}
 

3.2.3、二阶段Cancel

因为cancel和confirm实际是复用的同一段框架代码逻辑,只是对应状态不同,最终会路由到业务申明的cancel方法上

代码块
Java
 
 
 
 
 
private void cancelLocalTccTransaction(final LocalTccTransaction transaction,
                                       final SwanResourceManager tccResourceManager,
                                       final BranchContext context,
                                       final Throwable tryEx) throws SwanTransactionException {
  MetricsTemplate.executeWithoutCatTransaction(new MetricsTemplate.Executor() {
    @Override
    public Object execute() throws Throwable {
      RollbackRule rollbackRule = context.getRollbackRule();
      if (rollbackRule == null || rollbackRule.rollbackOn(tryEx)) {
        // 标记当前事务状态为NEED_CANCEL
        transaction.setStatus(GlobalStatus.NEED_CANCEL);
        transaction.setOnePhaseResult(JSON.toJSONString(context.getOnePhaseResultContext()));
        transaction.setMtraceParams(JSON.toJSONString(context.getMtraceParams()));
        transaction.setUpdateTime(System.currentTimeMillis());
        final LocalTccDomainConfig domainConfig = tccResourceManager.getLocalTccDomainConfig();
        if (domainConfig.getSyncCancel()) {
          // 同步 cancel
          syncCompensate(transaction, tccResourceManager);
        } else {
          // 非同步提交回滚需要计算下次执行时间并保存 mtrace 参数
          asyncCompensate(transaction, tccResourceManager, domainConfig);
        }
      } else {
        Cat.logEvent("Swan.noRollback", "TryException [" + tryEx.getClass().getName() + "], xid=" + transaction.getXid());
        transaction.setStatus(GlobalStatus.CANCEL_SUCCESS);
        tccResourceManager.getRepository().deleteLocalTccTransaction(transaction);
      }
      return null;
    }
  }, transaction, LocalMetrics.LOCAL_ROLLBACKED.getMetricsId(), tryEx);
}
 

4、公司内如何支持?

4.1、Swan

Swan是分布式事务框架(Distributed Transaction Framework)是美团点评基础架构团队独立研发的高性能,高可靠的分布式事务中间件,用来保障大规模分布式场景下业务数据的最终一致性。Swan 未来将广泛应用于美团点评内部交易,转账,发券,结算等核心链路,服务于亿级用户。其可以与公司内部各类基础中间件配套使用,轻松实现服务链路事务,跨库事务及各种组合。该框架具体实现了TCC远端模式、TCC本地模式、TXC模式,其中有借鉴Seata开源代码,主要特点是深度的融合了公司的主流中间件如报警监控、服务注册发现、链路跟踪、数据库中间件等,具体相关介绍见附录。

4.2、Mafka

 

Mafka在3.0中提供了分布式事务场景下的可靠消息支持,也就是说Mafka集群即承担了可靠消息服务的角色同时也承担了消息通知的角色,方案是通过Broker内的事务协调器组件接收生产者生产的事务消息,直接保存在本地日志文件中,根据生产端执行结果判断是否需要提交或丢弃消息。大致流程如下图:

  1. 生产端发送一条事务消息后事务协调器接收并记录消息和状态;

  2. 生产端执行业务逻辑后完成事务提交或回滚,并通知事务协调器;

  3. 事务协调器更新消息状态,确定是否将消息写到分区;

  4. 消费者消费对应消息,通过失败重试或私信方式完成消息的重试;

  5. 事务协调器会通过心跳来触发生产端的事务消息状态补偿;

​​

mafka可靠消息流程示意图

 

在Mafka中,通过以下三点保证了本地事务和消息最终被消费完成的最终一致性:

  • 多分区、持久化保证消息不丢失;

  • 消费端:重试+死信功能,保证消息一定被消费完成;

  • 事务协调服务:回查生产端,保证事务一定能结束;

4.3、Zebra

Zebra本身定位还是数据源管理,并不对分布式事务做实现,而是借助Swan的MTXC模式,以插件的形式实现了跨库事务的支持,首先Zebra给出了Sharding事务管理器通用接口ShardingTransactionManager,而Swan给出了支持分布式事务实现MtxcShardingTransactionManager,通过代理事务发起、事务提交和事务回滚实现对业务代码无侵入的跨库事务支持。因为Swan的MTXC工作模式借鉴于阿里的MTX工作模式,因此这里不做过多赘述,主要明确的是Zebra其实是通过Swan的插件化实现来支持跨库事务的,本质上还是基于Swan来实现。

​​

zebra跨库事务示意图

我们可以从上图发现,虽然是利用了TXC模式来支持Zebra的跨库事务特性,但是Swan也做了改动,主要是将TXC模式中的远端TC进行了轻量化,通过SDK形式内置到服务本地,以此提高整体的可用性,避免单点风险和网络延时风险。之所以这么选择,其实不难理解,Zebra的跨库事务都是本地多数据源调用,不涉及远端的RPC调用,这种场景下使用标准的远端TC部署模式,既没有必要还引入了额外的风险。

5、结合实际场景对比

5.1、场景介绍

在为智能版收银提供商户公众号托管服务的时候,业务上要求当商家在微信中取消授权给智能版收银之后,智能版收银中各个子业务线能够感知到取消授权操作,并执行各自的业务逻辑,如排队侧单独维护了线上POI与智能版收银商家公众号之间的绑定关系,在商户取消授权后需要进行绑定关系的解绑。由于架构设计的角度,智能版收银的商家公众号管理服务必然是和各个业务线业务服务进行解耦的,数据一致性的要求自然就产生了分布式事务的问题。下面看下简化的实际代码,便于理解。

代码块
Java
 
 
 
 
@Component
public class RmsWxMessageConsumer {
  @Autowired
  private WxMantaOpenRemoteService wxMantaOpenRemoteService;
  @Autowired
  private WxShopMappingService wxShopMappingService;

  // ......此处无关代码......
  // 如果要保证两次RPC要么同时执行成功要么同时不执行就需要解决分布式带来的问题
public void cancelBind(String authorizerAppId, long messageTime) throws TException {
    // ......此处省略过程处理代码......
    // 取消公众号和智能版租户的绑定【RPC,调用Manta】
    CommonResponse<Boolean> cancelResponse = wxMantaOpenRemoteService.cancelBizBinding(cancelAuthorizerBizBindingRequest);
    // ......此处省略过程处理代码......
    // 取消排队POI与公众号的绑定【RPC,调用排队】
    BaseResponse<Boolean> flushResponse = wxShopMappingService.flushBindingRelation(flushRequest);
    // ......此处省略过程处理代码......
  }
  // ......此处无关代码......
}
 

5.2、使用Swan TCC Remote模式

2、使用了注解,所以相关Try、Confirm、Cancel方法需要独立包装,没法复用给多个不同的分布式事务

此处省略相关的配置介绍,直接看下关键代码的改动,首先是事务发起方的代码结构,因为代码逻辑简单且成功率较高,因此直接在try阶段(cancelBind方法)中完成了业务逻辑的执行,二阶段提交则是空提交,只有二阶段回归做了一些业务补偿

代码块
Java
 
 
 
 
@Component
public class RmsWxMessageConsumer {

  @Autowired
  private WxMantaOpenRemoteService wxMantaOpenRemoteService;
  @Autowired
  private WxShopMappingService wxShopMappingService;

  // ......此处无关代码......
  // 事务配置,申明事务为TCC远端模式
  @SwanGlobalTransaction(name = "cancelBind",txType = SwanTxType.MTCC)
public void cancelBind(String authorizerAppId, long messageTime) throws TException {    
    // ......此处省略过程处理代码......
    // 取消公众号和智能版租户的绑定【RPC,调用Manta】
    CommonResponse<Boolean> cancelResponse = wxMantaOpenRemoteService.cancelBizBinding(cancelAuthorizerBizBindingRequest);
    // ......此处省略过程处理代码......
    // 取消排队POI与公众号的绑定【RPC,调用排队】
    BaseResponse<Boolean> flushResponse = wxShopMappingService.flushBindingRelation(flushRequest);
    // ......此处省略过程处理代码......
  }
}
 

因为是远端模式,所以下游事务参与方均需要实现配套的confirm、cancel方法,下面看下简化的代码,便于理解工作量:

代码块
Java
 
 
 
 
 
@Service
public class WxMantaOpenRemoteServiceImpl implements WxMantaOpenRemoteService {

  // 申明事务
  @SwanTccTransactional(domain = "remote.cancelBizBinding",confirmMethod = "cancelBizBindingConfirm",cancelMethod = "cancelBizBindingRevert",mode = "remote",antiSuspend = false)  
  public CommonResponse<Boolean> cancelBizBinding(CancelAuthorizerBizBindingRequest request) {
    // 将公众号与业务租户进行解绑
  }

  public void cancelBizBindingConfirm() {
    // 提交操作
  }

  public void cancelBizBindingRevert() {
    // 回滚操作
  }
}
 
代码块
Java
 
 
 
 
 
@Service
public class WxShopMappingServiceImpl implements WxShopMappingService {

  // 申明事务
  @SwanTccTransactional(domain = "remote.flushBindingRelation",confirmMethod = "flushBindingRelationConfirm",cancelMethod = "flushBindingRelationRevert",mode = "remote",antiSuspend = false)  
  public CommonResponse<Boolean> flushBindingRelation(CancelAuthorizerBizBindingRequest request) {
    // 将公众号与排队POI进行解绑
  }

  public boolean flushBindingRelationConfirm() {
    // 提交操作
  }

  public boolean flushBindingRelationRevert() {
    // 回滚操作
  }
}
 

可以发现TCC远端模式的开发工作量很高,因为所有的事务参与方都有开发工作量,且对代码侵入较高,这种模式一般指推荐在团队内跨服务使用。

5.3、使用Swan TCC Local模式

虽然下游不在需要改动,但是需要注意的是如果下游本身能力不丰富时可能还是需要有开发成本,比如下游业务方只提供了创建没有提供取消能力时

此处省略相关的配置介绍,直接看下关键代码的改动,首先是事务发起方的代码结构,因为代码逻辑简单且成功率较高,因此直接在try阶段(cancelBind方法)中完成了业务逻辑的执行,二阶段提交则是空提交,只有二阶段回归做了一些业务补偿

代码块
Java
 
 
 
 
@Component
public class RmsWxMessageConsumer {

  @Autowired
  private WxMantaOpenRemoteService wxMantaOpenRemoteService;
  @Autowired
  private WxShopMappingService wxShopMappingService;

  // ......此处无关代码......
  // 事务配置,不申明类型是默认为TCC本地模式
  @SwanLocalTccTransactional(doamin = "cancelBind", confirmMethod = "confirmCancel", cancelMethod = "revertCancel")
public void cancelBind(String authorizerAppId, long messageTime) throws TException {

    // ......此处省略过程处理代码......

    // 将业务信息放到事务上下文中,用于补偿时使用
    LocalTccContext context = TccTransactionContextUtil.getLocalContext();
    context.putMtraceParams("authorizerAppId", authorizerAppId);
    context.putMtraceParams("tenantId", String.valueOf(tanantId));
    context.putMtraceParams("queuePoiList", JSON.toJSONString(queuePoiList));

    // ......此处省略过程处理代码......
    // 取消公众号和智能版租户的绑定【RPC,调用Manta】
    CommonResponse<Boolean> cancelResponse = wxMantaOpenRemoteService.cancelBizBinding(cancelAuthorizerBizBindingRequest);
    // ......此处省略过程处理代码......
    // 取消排队POI与公众号的绑定【RPC,调用排队】
    BaseResponse<Boolean> flushResponse = wxShopMappingService.flushBindingRelation(flushRequest);
    // ......此处省略过程处理代码......
  }

  // 二阶段补偿
  // 执行一次空的确认,因为在一阶段已经执行完
  public boolean confirmMethod(int i) {
      tccTestService.confirm();
      return true;
  }
  // 二阶段回滚
  public boolean cancelMethod(boolean trySuccess, boolean conpensatorSuccess) {
    LocalTccContext context = TccTransactionContextUtil.getLocalContext();
    context.getMtraceParams("authorizerAppId", authorizerAppId);
    context.getMtraceParams("tenantId", String.valueOf(tanantId));
    context.putMtraceParams("queuePoiList", JSON.toJSONString(queuePoiList));

    // 根据上下文获取的信息进行业务数据回滚
    CommonResponse<Boolean> crateResponse = wxMantaOpenRemoteService.createBizBinding(createAuthorizerBizBindingRequest);
    BaseResponse<Boolean> flushResponse = wxShopMappingService.flushBindingRelation(flushRequest);
    return true;
  }

  // ......此处无关代码......
}
 

在TCC本地模式中下游无需进行改动,除非当前提供的功能不足以实现业务的补偿,这里不做代码展示。不难发现TCC本地模式最适合事务跨团队的场景。

5.4、使用Swan MTXC模式

代码块
Java
 
 
 
 
@Component
public class RmsWxMessageConsumer {
  @Autowired
  private WxMantaOpenRemoteService wxMantaOpenRemoteService;
  @Autowired
  private WxShopMappingService wxShopMappingService;

  // ......此处无关代码......
  // 配置事务类型为MTXC模式
  @SwanGlobalTransaction(name="${globalTransactionName}", txType = SwanTxType.MTXC)
public void cancelBind(String authorizerAppId, long messageTime) throws TException {
    // ......此处省略过程处理代码......
    // 取消公众号和智能版租户的绑定【RPC,调用Manta】
    CommonResponse<Boolean> cancelResponse = wxMantaOpenRemoteService.cancelBizBinding(cancelAuthorizerBizBindingRequest);
    // ......此处省略过程处理代码......
    // 取消排队POI与公众号的绑定【RPC,调用排队】
    BaseResponse<Boolean> flushResponse = wxShopMappingService.flushBindingRelation(flushRequest);
    // ......此处省略过程处理代码......
  }
  // ......此处无关代码......
}
 

业务方只需要配置数据源代理,没有代码开发成本,这里就不做展示,整体来说Swan MTXC模式对业务的侵入最低,这也限制了它只能做到SQL层面的事务提交和回滚。此外因为做了数据源代理,因此是实时上其实是对事务参与方产生了影响,因此如果事务参与方是公共服务或者高并发的服务不建议使用TXC模式。

5.5、小结

不难发现,通过框架解决分布式事务问题时,其实开发的工作成本很低,尤其对应TXC方案来说,基本没有代码调整,相对来说TCC模式就产生了一定的代码侵入,但是从另一个角度来看,如果回滚或者补偿涉及到了非数据层而是业务层逻辑时,MTXC无法胜任了。另外本节的例子同样可以使用可靠消息方法和最大努力通知方案完成,因为篇幅问题就不做介绍。

此外,如果根据经验并发场景不高或者涉及流程较少时,一般来说因为失败导致的数据不一致情况占比应该很低,此时完全可以通过先通过Cat报警等形式让开发人工介入,这样既节省人力也节省资源,当不一致情况多到影响业务或占用较多人力时,再考虑通过框架来处理分布式事务。

6、总结

本文主要介绍和讨论了分布式事务的常见解决方案,并结合代码详细介绍了两种框架的实现细节,之后简单分析了公司相关基础组件是如何支持分布式事务需求的。

最后做一个简单总结,通过上文我们可以发现分布式事务目前并没有十全十美的解决方案,针对不同的场景可能会有不同的解决方案,且多数都涉及到项目的改造,这就要求我们在使用分布式事务框架前需要明确使用的范围和必要性,明确成本与收益,如果只是很小概率的场景且对一致性没有高要求,完全可以考虑是否有其他可替代的传统方案,比如定时的补偿订正、或者就是简单的报警人工跟踪。如果确定要使用分布式事务框架后,我们可以依据“外柔内刚”的原则进行框架的选择:

  • 外柔:跨服务的分布式事务,很多涉及业务的都是长事务、异步场景多,可以采用可用性灵活度比较好的方案,如最大努力通知等;

  • 内刚:服务内的分布式事务多为短事务,一般是分库场景多,可以采用一致性比较好的方案,比如TXC等;

     

7、参考资料

XA协议

ACID

CAP理论 & BASE理论

Saga模式

Seata

Swan

 

posted @ 2022-04-08 14:32  刘尊礼  阅读(146)  评论(0)    收藏  举报