Seata实战-AT模式分布式事务原理、源码分析

前言

上回文章里简单介绍了分布式事务相关概念,并测试跑通了Seata中AT模式Demo和TCC模式Demo,对Seata不了解的可以先看下之前写的一篇简介文章

https://blog.csdn.net/hosaos/article/details/89136666

本着“知其然知其所以然”的态度,还是得分析下其原理及设计思路。本文会介绍下AT模式,即对业务零侵入方案的设计思路及原理,代码版本0.5.0

AT模式的核心是对业务无侵入,是一种改进后的两阶段提交,其设计思路如图

第一阶段

在这里插入图片描述
核心在于对业务sql进行解析,转换成undolog,并同时入库,这是怎么做的呢?先抛出一个概念DataSourceProxy代理数据源,通过名字大家大概也能基本猜到是什么个操作,后面做具体分析

第二阶段

分布式事务操作成功,则TC通知RM异步删除undolog
在这里插入图片描述
分布式事务操作失败,TM向TC发送回滚请求,RM 收到协调器TC发来的回滚请求,通过 XID 和 Branch ID 找到相应的回滚日志记录,通过回滚记录生成反向的更新 SQL 并执行,以完成分支的回滚
在这里插入图片描述
设计思路介绍完毕,下面开始发车盘源码

源码分析

入口之Seata集成Spring

源码分析,首先得找到切入点,不管是dubbo,mybatis,还是seata,为了让Java开发者更易上手使用,往往和Spring有紧密结合,比如实现spring bean里的InitializingBean接口在bean初始化完成后做一些初始化操作,使用拦截器对方法进行拦截,在拦截方法内做一些实现,Seata也是如此

先从Seata所需的Spring配置文件入手,看下Seata的使用需要哪些配置,看下官方Demo里的dubbo例子

https://github.com/fescar-group/fescar-samples

先看服务提供方dubbo-account-service.xml中的配置
在这里插入图片描述
可以看到有两个普通spring项目里没有的配置

  1. DataSourceProxy使用Seata中的代理数据源对普通数据源做一层代理,指定JdbcTemplate中的数据源为代理数据源
  2. 配置了一个名为GlobalTransactionScanner的bean,第一个构造参数为应用id,第二个参数为事务分组

GlobalTransactionScanner就是入口,看下其实现(去掉了构造方法)
在这里插入图片描述

可以看到分别实现了Spring的3个接口InitializingBeanApplicationContextAwareDisposableBean

关键点在afterPropertiesSet()中
在这里插入图片描述
调用了initClient方法
在这里插入图片描述
里面对TmClient,RmClient进行了初始化(参数就是配置文件bean里配置的applicationId和txServiceGroup),并注册了一个Spring的ShutdownHook

TmClient.init()

先看下再看下TmClient的初始化操作,其最终调用到的是TmRpcClient的init()方法,启动了一个定时器不断进行重连操作
在这里插入图片描述
先调用initVars()初始化了父类中一个消息超时的一个定时调度器,定时请求并将超时消息返回值设为null,不影响流程,代码不贴了

初始化了一个mergeSendExecutorService线程池,会将同一个fescar-server的消息合并发送(减少netty通信次数)

最终进到reconnect方法
在这里插入图片描述
先根据事务的分组名称获取到对应的seata-server的ip地址列表,然后进行重连

getAvailServerList中代码如下
在这里插入图片描述
RegistryFactory.getInstance().lookup(transactionServiceGroup)是针对不同注册中心做了适配的,默认看下File形式的实现
在这里插入图片描述
进到FileRegistryServiceImpl#lookup方法,这里结合File.conf配置来说明
在这里插入图片描述
在这里插入图片描述
1、现根据事务分组(key=vgroup_mapping.事务分组名称)找到分组所属的server集群名称,这里是default
2、然后根据集群名称(key=集群名称.grouplist)找到server对应ip端口地址

梳理下TmClient的初始化流程

  1. 启动ScheduledExecutorService定时执行器,每5秒尝试进行一次重连seata-server
  2. 重连时,先从file.conf中根据分组名称(service_group)找到集群名称(cluster_name)
  3. 再根据集群名称找到fescar-server集群ip端口列表
  4. 从ip列表中选择一个用netty进行连接

RmClient.init()

在这里插入图片描述
1、设置了资源管理器resourceManager
2、设置了消息回调监听器,rmHandler用于接收fescar-server在二阶段发出的提交或者回滚请求

RmClient初始化时用到了Java Spi拓展机制,Seata中对ResourceManagerAbstractRMHandler做了SPI适配,以ResouceManager为例说明

在这里插入图片描述
可以看到初始化DefaultResouceManager时会使用ClassLoader去加载对应Jar下的实现,而默认AT模式使用的实现是数据库,也就是rm-datasource包下的实现,找实现类路径需要定位到/resources/META-INF/扩展接口全路径去找
在这里插入图片描述
这样就找到了对应实现类的全路径

  1. ResourceManager对应实现类全路径 io.seata.rm.datasource.DataSourceManager,该类中指定了了提交和回滚的方法
  2. DefaultRMHandler对应实现类全路径io.seata.rm.RMHandlerAT,该类在二阶段代码分析过程再做细讲,只需先记住是个接收server消息并做对应提交或者回滚操作的回调处理类

init()方法,和TmClient初始化过程基本一致,不再重复贴代码

做个总结:

  1. Spring启动时,初始化了2个客户端TmClient、RmClient
  2. TmClient与Server通过Netty建立连接并发送消息
  3. RmClient与Server通过Netty建立连接,负责接收二阶段提交、回滚消息并在回调器(RmHandler)中做处理

下面具体进行到全局事务的两阶段提交过程中做分析

第一阶段

拦截器中开启事务

在需要加全局事务的方法中,会加上GlobalTransactional注解,注解往往对应着拦截器,Seata中拦截全局事务的拦截器是GlobalTransactionalInterceptor
看下其拦截方法
在这里插入图片描述
判断:

  • 如果方法上有全局事务注解,调用handleGlobalTransaction开启全局事务
  • 如果没有,按普通方法执行,避免性能下降

看下handleGlobalTransaction()方法
在这里插入图片描述
可以看到最终调用的是TransactionalTemplate的execute方法,execute方法如下
在这里插入图片描述
分为几步

  1. 开启全局事务beginTransaction
  2. 执行业务方法
  3. 提交事务commitTransaction(若没抛异常)
  4. 执行completeTransactionAfterThrowing回滚操作(抛异常)

beginTransaction最终调用到了io.seata.tm.api.DefaultGlobalTransaction#begin(int, java.lang.String)方法,代码如下
在这里插入图片描述

  1. 调用transactionManager.begin()方法通过TmRpcClient与server通信并生成一个xid
  2. 将xid绑定到Root上下文中

看到这里,也就明确了一点,全局事务开启时,是由TM来发起的

commitTransaction方法类似,由TM发送事务commit信息给seata-server,略去源码

sql解析与undolog生成

全局事务拦截成功后最终还是执行了业务方法的,但是由于Seata对数据源做了代理,所以sql解析与undolog入库操作是在数据源代理中执行的,箭头处的代理就是Seata对DataSource,Connection,Statement做的代理封装类
在这里插入图片描述
最终对Sql进行解析操作,发生在StatementProxy类中
在这里插入图片描述
交给了ExecuteTemplate执行,跟到ExecuteTemplate中
在这里插入图片描述

流程如下

  1. 先判断是否开启了全局事务,如果没有,不走代理,不解析sql,避免性能下降
  2. 调用SQLVisitorFactory对目标sql进行解析
  3. 针对特定类型sql操作(INSERT,UPDATE,DELETE,SELECT_FOR_UPDATE)等进行特殊解析
  4. 执行sql并返回结果

关键点在于特定类型执行器中的execute方法,先看下类继承图
在这里插入图片描述
挑选InsertExecutor为例说明,其execute方法调用的是父类BaseTransactionalExecutor中的execute方法,看下源码
在这里插入图片描述
将ROOT上下文中的xid绑定到了connectionProxy中,并调用了doExecute方法,看下AbstractDMLBaseExecutor中的doExecute方法
在这里插入图片描述

在这里插入图片描述
executeAutoCommitTrue中先将autoCommit设置为false(因为要对sql进行解析,生成undolog在一个事务中入库,避免提前入库)

再执行到executeAutoCommitFalse中,分为4步

  1. 获取sql执行前镜像beforeImage
  2. 执行sql
  3. 获取sql执行后afterimage
  4. 根据beforeImage,afterImage生成undolog记录并添加到connectionProxy的上下文中

到此为止,红色框中几步已经完成
在这里插入图片描述

分支事务注册与事务提交

业务sql执行以及undolog执行完后会在ConnectionProxy中执行commit操作
看下代码
在这里插入图片描述
1、如果处于全局事务中,则调用processGlobalTransactionCommit()处理全局事务提交
2、如果加了全局锁注解,加全局锁并提交
3、如果没有对应注释,按直接进行事务提交

主要看processGlobalTransactionCommit()方法,也是核心代码
在这里插入图片描述
流程分为如下几步

  1. 注册分支事务register(),并将branchId分支id绑定到上下文中
    在这里插入图片描述
  2. UndoLogManager.flushUndoLogs(this) 如果包含undolog,则将之前绑定到上下文中的undolog进行入库
  3. 提交本地事务
  4. 如果操作失败,report()中通过RM提交第一阶段失败消息,如果成功,report()提交第一阶段成功消息

在这里插入图片描述
undolog入库和普通业务sql的执行用的一个connection,处于一个本地事务中,保证了业务数据变更时,一定会有对应undolog存在

至此,第一阶段中undolog提交与本地事务提交,分支事务注册与汇报也已完成
在这里插入图片描述

第二阶段

在前面分析RmClient.init()方法时,提到了Seata会使用SPI拓展机制找到RmClient的回调处理器RMHandlerAT,该类是负责接送二阶段seata-server发给RmClient的提交、回滚消息,并作出提交,回滚操作
在这里插入图片描述

先看下RMHandlerAT继承自AbstractRMHandler,AbstractRMHandler中两个handle方法对应,事务提交、回滚操作

全局事务提交

对应了doBranchCommit(request, response)方法
在这里插入图片描述
调用的是getResourceManager(),上面提到SPI拓展提到的DataSourceManager类
在这里插入图片描述
DataSourceManager中调用了asyncWorker来异步提交,看下AsyncWorker中branchCommit方法
在这里插入图片描述
这边只是往一个ASYNC_COMMIT_BUFFER缓冲List中新增了一个二阶段提交的context

但真正提交在哪呢?答案在AsyncWorker的init()方法中,其init()方法会在DataSourceManager中被调用,内部启动一个定时器不断进行全局事务提交操作
在这里插入图片描述
终于跟到了真正的分支事务提交方法中

在这里插入图片描述
分为几步

  1. 先按resourceId(也就是数据连接)对提交操作进行分组,一个数据库的可以一起操作,提升效率
  2. 根据resourceId找到对应DataSourceProxy,并获取一个普通的数据库连接getPlainConnection(),估计这本身不需要做代理操作,故用了普通的数据库连接
  3. 调用UndoLogManager.deleteUndoLog(commitContext.xid, commitContext.branchId, conn)删除undolog

回过头来看下设计原理图
在这里插入图片描述

全局事务回滚

同样的,从io.seata.rm.AbstractRMHandler#doBranchRollback跟到io.seata.rm.datasource.DataSourceManager#branchRollback中,最终回滚方法调用的是UndoLogManager.undo(dataSourceProxy, xid, branchId);
在这里插入图片描述

具体代码如下
在这里插入图片描述

具体根据Undolog进行反解析操作实现在AbstractUndoExecutor的子类中,有兴趣的可以自己看

然后
如果判断undolog存在exists,则删除对应undolog,并一并提交
在这里插入图片描述

这时候 再看下这回滚设计原理图,是不是清晰了很多
在这里插入图片描述

发布了43 篇原创文章 · 获赞 134 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/hosaos/article/details/89403552