[图文] Seata AT 模式分布式事务源码分析

  1. 推荐阅读 Seata TCC 分布式事务源码分析
  2. 公众号 Young_Blog

什么是 Seata AT 模式

AT 模式是 Seata 主推的分布式事务解决方案,最早来源于阿里中间件团队发布的 TXC 服务,后来成功上云改名 GTSSeata 官方文档中有关于 AT 模式的详细介绍 —— AT Mode,它使得应用代码可以像使用本地事务一样使用分布式事务,完全屏蔽了底层细节,它和笔者之前介绍过的 Seata TCC 模式的区别有以下几点:

  1. 使用上,TCC 依赖于用户自行实现的三个方法成本较大;AT 依赖全局事务注解和代理数据源,其余代码基本不需要改动,对业务无侵入、接入成本极小
  2. TCC 的作用范围在应用层,本质上是实现针对某种业务逻辑的正向和反向方法;AT 模式的作用范围在于底层数据源,通过保存操作行记录的前后快照和生成反向 SQL 语句进行补偿操作,实现难度较大,优点是对上层应用透明
  3. TCCtry 阶段加锁,后续补偿逻辑事务间各自独立;AT 需要借助于全局锁和 GlobalLock 注解来解决不同全局事务间的写冲突问题,如果一阶段分支事务成功则二阶段一开始全局锁即被释放,否则需要夯住直到分支事务二阶段回滚完成才能释放全局锁

Seata AT 的使用方法

我们先了解一下如何在应用里使用 AT 模式,流程非常简单,Seata 也提供了 Seata-Samples 方便大家了解如何使用该项目。

第一步,增加全局事务注解

首先依赖 Seata 的客户端 SDK,然后在整个分布式事务发起方的业务方法上增加 @GlobalTransactional 注解,下面的例子来源于 Seata-Samples dubbo 案例,purchase 是事务发起方的业务方法,通过 RPC 调用了下游库存服务订单服务提供的接口:

@Override
@GlobalTransactional(timeoutMills = 300000, name = "dubbo-demo-tx")
public void purchase(String userId, String commodityCode, int orderCount) {
   
    LOGGER.info("purchase begin ... xid: " + RootContext.getXID());
    // RPC 调用库存服务
    storageService.deduct(commodityCode, orderCount);
    // RPC 调用订单服务
    orderService.create(userId, commodityCode, orderCount);
    throw new RuntimeException("xxx");
}
第二步,配置代理数据源

MySQL 为例:

// 配置数据源
<bean name="accountDataSource" class="com.alibaba.druid.pool.DruidDataSource"
        init-method="init" destroy-method="close">
        // …… …… 省略数据源配置
</bean>

// 关键步骤,配置 Seata 的代理数据源,代理之前配置的 accountDataSource
<bean id="accountDataSourceProxy" class="io.seata.rm.datasource.DataSourceProxy">
    <constructor-arg ref="accountDataSource" />
</bean>

// 配置 applicationId 和 txServiceGroup,这主要是来标识应用和服务端集群的
<bean class="io.seata.spring.annotation.GlobalTransactionScanner">
    <constructor-arg value="dubbo-demo-account-service"/>
    <constructor-arg value="my_test_tx_group"/>
</bean>

// 省略一些 dubbo 服务注册配置和 jdbcTemplate 配置
第三步,新建 undo_log 表

在事务链涉及的服务的数据库中新建 undo_log 表用来存储 UndoLog 信息,用于二阶段回滚操作,表中包含 xidbranchIdrollback_info 等关键字段信息。

Seata AT 的工作流程

工作流程总览

概括来讲,AT 模式的工作流程分为两阶段。一阶段进行业务 SQL 执行,并通过 SQL 拦截、SQL 改写等过程生成修改数据前后的快照(Image),并作为 UndoLog 和业务修改在同一个本地事务中提交

如果一阶段成功那么二阶段仅仅异步删除刚刚插入的 UndoLog;如果二阶段失败则通过 UndoLog 生成反向 SQL 语句回滚一阶段的数据修改。其中关键的 SQL 解析和拼接工作借助了 Druid Parser 中的代码,这部分本文并不涉及,感兴趣的小伙伴可以去翻看源码,并不是很复杂

图解 AT 模式一阶段流程

一阶段中分支事务的具体工作有:

  1. 根据需要执行的 SQLUPDATEINSERTDELETE)类型生成相应的 SqlRecognizer
  2. 进而生成相应的 SqlExecutor
  3. 接着便进入核心逻辑查询数据的前后快照,例如图中标红的部分,拿到修改数据行的前后快照之后,将二者整合生成 UndoLog,并尝试将其和业务修改在同一事务中提交。

整个流程的流程图如下:

值得注意的是,本地事务提交前必须先向服务端注册分支,分支注册信息中包含由表名和行主键组成的全局锁,如果分支注册过程中发现全局锁正在被其他全局事务锁定则抛出全局锁冲突异常,客户端需要循环等待,直到其他全局事务释放锁之后该本地事务才能提交。Seata 以这样的机制保证全局事务间的写隔离。

图解二阶段 Commit 流程

对服务端来说,等到一阶段完成未抛异常,全局事务的发起方会向服务端申请提交这个全局事务,服务端根据 xid 查询出该全局事务后加锁并关闭这个全局事务,目的是防止该事务后续还有分支继续注册上来,同时将其状态从 Begin 修改为 Committing

紧接着,判断该全局事务下的分支类型是否均为 AT 类型,若是则服务端会进行异步提交,因为 AT 模式下一阶段完成数据已经落地。服务端仅仅修改全局事务状态为 AsyncCommitting,然后会有一个定时线程池去存储介质(File 或者 Database)中查询出待提交的全局事务日志进行提交,如果全局事务提交成功则会释放全局锁并删除事务日志。整个流程如下图所示:

对客户端来说,先是接收到服务端发送的 branch commit 请求,然后客户端会根据 resourceId 找到相应的 ResourceManager,接着将分支提交请求封装成 Phase2Context 插入内存队列 ASYNC_COMMIT_BUFFER,客户端会有一个定时线程池去查询该队列进行 UndoLog 的异步删除。

一旦客户端提交失败或者 RPC 超时,则服务端会将该全局事务状态置位 CommitRetrying,之后会由另一个定时线程池去一直重试这些事务直至成功。整个流程如下图所示:

图解二阶段 Rollback 流程

回滚相对复杂一些,如果发起方一阶段抛异常会向服务端请求回滚该全局事务,服务端会根据 xid 查询出这个全局事务,加锁关闭事务使得后续不会再有分支注册上来,并同时更改其状态 BeginRollbacking,接着进行同步回滚以保证数据一致性。除了同步回滚这个点外,其他流程同提交时相似,如果同步回滚成功则释放全局锁并删除事务日志,如果失败则会进行异步重试。整个流程如下图所示:

客户端接收到服务端的 branch rollback 请求,先根据 resourceId 拿到对应的数据源代理,然后根据 xidbranchId 查询出 UndoLog 记录,反序列化其中的 rollback 字段拿到数据的前后快照,我们称该全局事务为 A

根据具体 SQL 类型生成对应的 UndoExecutor,校验一下数据 UndoLog 中的前后快照是否一致或者前置快照和当前数据(这里需要 SELECT 一次)是否一致,如果一致说明不需要做回滚操作,如果不一致则生成反向 SQL 进行补偿,在提交本地事务前会检测获取数据库本地锁是否成功,如果失败则说明存在其他全局事务(假设称之为 B)的一阶段正在修改相同的行,但是由于这些行的主键在服务端已经被当前正在执行二阶段回滚的全局事务 A 锁定,因此事务 B 的一阶段在本地提交前尝试获取全局锁一定是失败的,等到获取全局锁超时后全局事务 B 会释放本地锁,这样全局事务 A 就可以继续进行本地事务的提交,成功之后删除本地 UndoLog 记录。整个流程如下图所示:

本节小结

我们通过流程图分析了一下 Seata AT 模式两阶段的工作流程,这里提一句,官方文档针对 AT 模式的工作流程提供了一个非常易懂的例子 —— AT 模式工作机制

笔者强烈建议感兴趣的同学阅读过后,再看下文的源码分析

Seata AT 模式源码模块拆解

通过上面的文字和图解,相信大家已经了解了 Seata AT 模式的基本工作原理,那么本节开始我们正式进入相关源码的分析阶段。第一步,由于 Seata 模块不算少,我们先对整个 Seata 项目的模块进行拆解,挑出其中需要重点关注的模块,忽略那些次要的。

下文的源码分析均基于 Seata v0.6.1 版本

相比于之前笔者对 Seata TCC 实现的分析,AT 模式的源码就要复杂很多了,基本上大多数模块均有涉及,因此在阅读源码之前,我们先对模块的优先级进行筛选,包括下文会叙述哪些模块和忽略哪些模块

首先,seata-tccAT 的功能无关可以不用看;seata-commonseata-coreseata-configseata-discovery 这些只看名字也能知道大致的功能,后续阅读代码期间经常会看到其中的类,因此都可以暂时忽略seata-tmseata-rm 这两者都是封装的与 seata-server 进行通信的方法和步骤,这部分笔者已经在上一篇关于 TCC 的文章中叙述过了,不再赘述seata-spring 主要是注解、切面织入、方法拦截等功能的实现,关键点包括全局事务的开启,但是由于 ATTCC 在全局事务开启部分的逻辑是一致的,因此本文也不再赘述

一通排查下来,和 AT 核心功能有关的模块仅剩下 seata-rm-datasourceseata-server,仔细一想这也很合理,因为 Seata 中分支事务才是真正执行数据修改和补偿的部分,因此对于 TCC 模式来说,TwoPhaseBusinessAction 注解的实现类是分支事务,对 AT 模式来说,代理数据源正是分支事务,因此核心逻辑必然在 seata-rm-datasource 模块中,而 TC 集群是协调整个全局事务的指挥者,自然 seata-server 模块也是我们需要特别关注的,但是由于服务端逻辑和 TCC 部分高度相似,除了 v0.6.1 中新增了 DB 模式作为日志存储介质外,因此下文先选取客户端 AT 模式相关源码进行深入分析,最后简要分析下与 AT 模式相关的服务端源码

Seata AT 模式客户端部分

数据源代理部分 —— 三类 Proxy

下图来源于 Seata 官方文档:

Seata 中主要针对 java.sql 包下的 DataSourceConnectionStatementPreparedStatement 四个接口进行了再包装,包装类分别为 DataSourceProxyConnectionProxyStatementProxyPreparedStatementProxy,很好一一对印,其功能是在 SQL 语句执行前后、事务 commit 或者 rollbakc 前后进行一些与 Seata 分布式事务相关的操作,例如分支注册、状态回报、全局锁查询、快照存储、反向 SQL 生成等。

ExecuteTemplate.execute

AT 模式下,真正分支事务开始是在 StatementProxyPreparedStatementProxyexecuteexecuteQueryexecuteUpdate具体执行方法中,这些方法均实现自 StatementPreparedStatement 的标准接口,而方法体内调用了 ExecuteTemplate.execute方法拦截,下面我们来看看这个方法的实现:

public static <T, S extends Statement> T execute(SQLRecognizer sqlRecognizer,
                                                    StatementProxy<S> statementProxy,
                                                    StatementCallback<T, S> statementCallback,
                                                    Object... args) throws SQLException {
   
    
    // 如果不是处于全局事务中,即上游没有 xid 传递下来
    // 或者没有 GlobalLock 修饰,该数据操作不需要纳入 Seata 框架下进行管理
    // 则直接执行这个 SQL                                                        
    if (!RootContext.
  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值