• XID:全局事务的唯一标识,由 ip:port:sequence 组成;
  • Transaction Coordinator (TC):事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚;
  • Transaction Manager (TM ):控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议;
  • Resource Manager (RM):控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚;

业务逻辑是经典的下订单、扣余额、减库存流程。 根据模块划分为三个独立的服务,且分别连接对应的数据库:

  • 订单:order-server
  • 账户:account-server
  • 库存:storage-server
  • 业务:business-server

项目结构如下图

img

正常业务:

  1. business发起购买请求

  2. storage扣减库存

  3. order创建订单

  4. account扣减余额

异常业务:

扣减余额异常

正常流程下 2、3、4 步的数据正常更新全局 commit,异常流程下的数据则由于第 4 步的异常报错全局回滚。

使用流程及解析

  1. 首先创建相应的数据库与数据表
库名含有的表作用
db_accountaccount_tbl undo_log存储用户信息
db_orderorder_tbl undo_log订单信息
db_storagestorage_tbl undo_log库存信息

注:上面的每一个库可以存储与不同的机器上面。需要在不同的服务里面配置即可

  • 启动TC:

TC是用于协调全局事务和分支事务的,用于驱动commit和rollback

  • 启动各个服务:

img

TM服务是位于business-service中的。

img

其中怕purchase使用@GlobalTransactional注解,因此它是作为整个事务的TM。

其中TM的处理流程的代码位于GlobalTransactionalInterceptor.java

img

handleGlobalTransaction 方法中对 TransactionalTemplate 的 execute 进行了调用,从类名可以看到这是一个标准的模版方法,它定义了 TM 对全局事务处理的标准步骤。

img

首先开启全局事务然后执行相应的业务逻辑,如成功则commit、失败则进入相应的处理程序。

在DefaultGlobalTransaction中

img

方法开头处 if (role != GlobalTransactionRole.Launcher) 对 role 的判断有关键的作用,表明当前是全局事务的发起者(Launcher)还是参与者(Participant)。如果在分布式事务的下游系统方法中也加上 @GlobalTransactional 注解,那么它的角色就是 Participant,会忽略后面的 begin 直接 return,而判断是 Launcher 还是 Participant 是根据当前上下文是否已存在 XID 来判断,没有 XID 的就是 Launcher,已经存在 XID的就是 Participant。由此可见,全局事务的创建只能由 Launcher 执行,而一次分布式事务中也只有一个Launcher 存在。

DefaultTransactionManager 负责 TM 与 TC 通讯,发送 begin、commit、rollback 指令。

img

img

可以看出在最开始的时候TM需要发起一个全局事务的请求。

img

TC返回了一个全局事务ID,TM成功的注册了一个全局事务

全局事务创建后,就开始执行 business.execute(),即业务代码 storageFeignClient.deduct(commodityCode, orderCount) 进入 RM 处理流程,此处的业务逻辑为调用 storage-service 的扣减库存接口。

img

RM事务处理流程

Orderserveice代码

img

storage 的接口和 service 方法并未出现 seata相关的代码和注解,体现了 seata的无侵入。那它是如何加入到这次全局事务中的呢?答案在 ConnectionProxy 中,这也是前面说为什么必须要使用 DataSourceProxy 的原因,通过 DataSourceProxy 才能在业务代码的本地事务提交时,fescar 通过该切入点,向 TC 注册分支事务并发送 RM 的处理结果。

由于业务代码本身的事务提交被 ConnectionProxy 代理实现,所以在提交本地事务时,实际执行的是ConnectionProxy 的 commit 方法。

img

img

通过日志打印可以看出:

img

获取business-service传来的XID

img

  1. 绑定XID到当前上下文中

  2. 执行业务逻辑sql

  3. 向TC创建本次RM的Netty连接

  4. 向TC发送分支事务的相关信息

  5. 获得TC返回的branchId

  6. 记录Undo Log数据

  7. 向TC发送本次事务PhaseOne阶段的处理结果

  8. 从当前上下文中解绑XID

其中第 1 步和第 9 步,是在 SeatarHandlerInterceptor 中完成的,该类并不属于 seata,是前面提到的 spring-cloud-alibaba-fescar,它实现了基于 feign、rest 通信时将 xid bind 和 unbind 到当前请求上下文中。到这里 RM 完成了 PhaseOne 阶段的工作,接着看 PhaseTwo 阶段的处理逻辑。

事务提交

各分支事务执行完成后,TC 对各 RM 的汇报结果进行汇总,给各 RM 发送 commit 或 rollback 的指令

img

从日志中可以看出

  1. RM 收到 commit 通知;
  2. 执行 commit 动作;
  3. 将 commit 结果发送给 TC,branchStatus 为 PhaseTwo_Committed;

具体看下二阶段 commit 的执行过程,在 AbstractRMHandler 类的 doBranchCommit 方法:

img

最终会将 branchCommit 的请求调用到 AsyncWorker 的 branchCommit 方法。AsyncWorker 的处理方式是fescar 架构的一个关键部分,因为大部分事务都是会正常提交的,所以在 PhaseOne 阶段就已经结束了,这样就可以将锁最快的释放。PhaseTwo 阶段接收 commit 的指令后,异步处理即可。将 PhaseTwo 的时间消耗排除在一次分布式事务之外。

所以对于commit动作的处理,RM只需删除xid、branchId对应的undo_log即可。

img

事务回滚

对于rollback场景的触发有两种情况

  1. 分支事务处理异常,即 ConnectionProxyreport(false) 的情况
  2. TM捕获到下游系统上抛的异常,即发起全局事务标有 @GlobalTransactional 注解的方法捕获到的异常。在前面 TransactionalTemplate 类的execute模版方法中,对business.execute()的调用进行了catch,catch后会调用rollback,由TM通知TC对应XID需要回滚事务

img

TC 汇总后向参与者发送 rollback 指令,RM 在 AbstractRMHandler 类的 doBranchRollback 方法中接收这个rollback 的通知。

img

然后将 rollback 请求传递到 DataSourceManager 类的 branchRollback 方法

库存的rollback第一阶段

img

库存的rollback第二阶段

img

Business中的rollback的日志:

img

从日志中也可以看出rollback成功。