上回文章里简单介绍了分布式事务相关概念,并测试跑通了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 并执行,以完成分支的回滚
设计思路介绍完毕,下面开始发车盘源码
源码分析,首先得找到切入点,不管是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项目里没有的配置
DataSourceProxy
使用Seata中的代理数据源对普通数据源做一层代理,指定JdbcTemplate中的数据源为代理数据源GlobalTransactionScanner
的bean,第一个构造参数为应用id,第二个参数为事务分组GlobalTransactionScanner
就是入口,看下其实现(去掉了构造方法)
可以看到分别实现了Spring的3个接口InitializingBean
,ApplicationContextAware
,DisposableBean
关键点在afterPropertiesSet()中
调用了initClient方法
里面对TmClient,RmClient进行了初始化(参数就是配置文件bean里配置的applicationId和txServiceGroup),并注册了一个Spring的ShutdownHook
先看下再看下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、设置了资源管理器resourceManager
2、设置了消息回调监听器,rmHandler用于接收fescar-server在二阶段发出的提交或者回滚请求
RmClient初始化时用到了Java Spi拓展机制,Seata中对ResourceManager
,AbstractRMHandler
做了SPI适配,以ResouceManager为例说明
可以看到初始化DefaultResouceManager时会使用ClassLoader去加载对应Jar下的实现,而默认AT模式使用的实现是数据库,也就是rm-datasource包下的实现,找实现类路径需要定位到/resources/META-INF/扩展接口全路径
去找
这样就找到了对应实现类的全路径
io.seata.rm.datasource.DataSourceManager
,该类中指定了了提交和回滚的方法io.seata.rm.RMHandlerAT
,该类在二阶段代码分析过程再做细讲,只需先记住是个接收server消息并做对应提交或者回滚操作的回调处理类init()方法,和TmClient初始化过程基本一致,不再重复贴代码
做个总结:
下面具体进行到全局事务的两阶段提交过程中做分析
在需要加全局事务的方法中,会加上GlobalTransactional
注解,注解往往对应着拦截器,Seata中拦截全局事务的拦截器是GlobalTransactionalInterceptor
看下其拦截方法
判断:
handleGlobalTransaction
开启全局事务看下handleGlobalTransaction()方法
可以看到最终调用的是TransactionalTemplate
的execute方法,execute方法如下
分为几步
beginTransaction最终调用到了io.seata.tm.api.DefaultGlobalTransaction#begin(int, java.lang.String)方法,代码如下
看到这里,也就明确了一点,全局事务开启时,是由TM来发起的
commitTransaction方法类似,由TM发送事务commit信息给seata-server,略去源码
全局事务拦截成功后最终还是执行了业务方法的,但是由于Seata对数据源做了代理,所以sql解析与undolog入库操作是在数据源代理中执行的,箭头处的代理就是Seata对DataSource,Connection,Statement做的代理封装类
最终对Sql进行解析操作,发生在StatementProxy
类中
交给了ExecuteTemplate
执行,跟到ExecuteTemplate中
流程如下
SQLVisitorFactory
对目标sql进行解析关键点在于特定类型执行器中的execute方法,先看下类继承图
挑选InsertExecutor
为例说明,其execute方法调用的是父类BaseTransactionalExecutor
中的execute方法,看下源码
将ROOT上下文中的xid绑定到了connectionProxy中,并调用了doExecute方法,看下AbstractDMLBaseExecutor中的doExecute方法
executeAutoCommitTrue中先将autoCommit设置为false(因为要对sql进行解析,生成undolog在一个事务中入库,避免提前入库)
再执行到executeAutoCommitFalse中,分为4步
业务sql执行以及undolog执行完后会在ConnectionProxy中执行commit操作
看下代码
1、如果处于全局事务中,则调用processGlobalTransactionCommit()处理全局事务提交
2、如果加了全局锁注解,加全局锁并提交
3、如果没有对应注释,按直接进行事务提交
主要看processGlobalTransactionCommit()方法,也是核心代码
流程分为如下几步
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中被调用,内部启动一个定时器不断进行全局事务提交操作
终于跟到了真正的分支事务提交方法中
同样的,从io.seata.rm.AbstractRMHandler#doBranchRollback
跟到io.seata.rm.datasource.DataSourceManager#branchRollback
中,最终回滚方法调用的是UndoLogManager.undo(dataSourceProxy, xid, branchId);
具体根据Undolog进行反解析操作实现在AbstractUndoExecutor的子类中,有兴趣的可以自己看