
Shardingsphere整合Atomikos对XA分布式事务的支持(2)
Apache ShardingSphere 是一套开源的分布式数据库中间件解决方案组成的生态圈,它由 JDBC、Proxy 和 Sidecar(规划中)这 3 款相互独立,却又能够混合部署配合使用的产品组成。它们均提供标准化的数据分片、分布式事务和数据库治理功能,可适用于如 Java 同构、异构语言、云原生等各种多样化的应用场景。
ShardingSphere 已于2020年4月16日成为 Apache 软件基金会的顶级项目。
咱们话不多,接上篇,我们直接进入正题。
Atomikos简单介绍
Atomikos(https://www.atomikos.com/),其实是一家公司的名字,提供了基于JTA规范的XA分布式事务TM的实现
。其旗下最著名的产品就是事务管理器。产品分两个版本:
- TransactionEssentials:开源的免费产品;
- ExtremeTransactions:上商业版,需要收费。
这两个产品的关系如下图所示:
ExtremeTransactions在TransactionEssentials的基础上额外提供了以下功能(重要的):
- 支持TCC:这是一种柔性事务
- 支持通过RMI、IIOP、SOAP这些远程过程调用技术,进行事务传播。
- 事务日志云存储,云端对事务进行恢复,并且提供了完善的管理后台。
org.apache.shardingsphere.transaction.xa.XAShardingTransactionManager详解
我们简单的来回顾下org.apache.shardingsphere.transaction.spiShardingTransactionManager
我们重点县关注init
方法,从它的命名,你就应该能够看出来,这是整个框架的初始化方法,让我们来看看它是如何进行初始化的。
- 首先SPI的方式加载XATransactionManager的具体实现类,这里返回的就是
org.apache.shardingsphere.transaction.xa.atomikos.manager.AtomikosTransactionManager
。
- 我们在关注下
new XATransactionDataSource()
, 进入org.apache.shardingsphere.transaction.xa.jta.datasource。XATransactionDataSource
类的构造方法。
- 我们重点来关注
XADataSourceFactory.build(databaseType, dataSource)
,从名字我们就可以看出,这应该是返回JTA规范里面的XADataSourc
,在ShardingSphere里面很多的功能,可以从代码风格的命名上就能猜出来,这就是优雅代码(吹一波)。不多逼逼,我们进入该方法。
- 首先又是一个SPI定义的
XADataSourceDefinitionFactory
,它根据不同的数据库类型,来加载不同的方言。然后我们进入swap
方法。
- 很简明,第一步创建,
XADataSource
,第二步给它设置属性(包含数据的连接,用户名密码等),然后返回。
- 返回
XATransactionDataSource
类,关注xaTransactionManager.registerRecoveryResource(resourceName, xaDataSource);
从名字可以看出,这是注册事务恢复资源。这个我们在事务恢复的时候详解。
- 返回
XAShardingTransactionManager.init()
,我们重点来关注:
xaTransactionManager.init();
,最后进入AtomikosTransactionManager.init()
- 进入
UserTransactionServiceImp.init()
- 我们重点关注,获取配置属性。最后进入
com.atomikos.icatch.provider.imp.AssemblerImp.initializeProperties()
方法
- 接下来重点关注,
Configuration.init()
, 进行初始化。
- 我们先来关注
assembleSystemComponents(configProperties);
进入它,进入com.atomikos.icatch.provider.imp.AssemblerImp.assembleTransactionService()
方法:
- 我们重点来分析
createOltpLogFromClasspath()
, 采用SPI的加载方式来获取,默认这里会返回null
, 什么意思呢?
就是当没有扩展的时候,atomikos,会创建框架自定义的资源,来存储事务日志。
- 我们跟着进入
Repository repository = createRepository(configProperties);
- 这里就会创建出
CachedRepository
,里面包含了InMemoryRepository
与FileSystemRepository
- 回到主线
com.atomikos.icatch.config.Configuration.init()
, 最后来分析下notifyAfterInit();
- 插件的初始化会进入
com.atomikos.icatch.jta.JtaTransactionServicePlugin.afterInit()
- 重点注意
RecoveryLog recoveryLog = Configuration.getRecoveryLog();
,如果用户采用SPI的方式
,扩展了com.atomikos.recovery.OltpLog
,这里就会返回 null
。如果是null,则不会对XaResourceRecoveryManager
进行初始化。
- 回到
notifyAfterInit()
, 我们来分析setRecoveryService
。
- 我们进入
recover()
方法:
- 看到最关键的注释了吗,如果用户采用
SPI的方式
,扩展了com.atomikos.recovery.OltpLog
,那么XaResourceRecoveryManager
为null,则就会进行云端恢复,反之则进行事务恢复。事务恢复很复杂,我们会单独来讲。
到这里atomikos的基本的初始化已经完成。
atomikos事务begin流程
我们知道,本地的事务,都会有一个 trainsaction.begin
, 对应XA分布式事务来说也不另外,我们再把思路切换回XAShardingTransactionManager.begin()
, 会调用com.atomikos.icatch.jta.TransactionManagerImp.begin()
- 这里我们主要关注
compositeTransactionManager.createCompositeTransaction()
,
- 创建了事务补偿点,然后把他放到了用当前线程作为key的Map当中,这里思考,
为啥它不用 threadLocal
。
到这里atomikos的事务begin流程已经完成。大家可能有些疑惑,begin好像什么都没有做,XA start 也没调用?别慌,下一节继续来讲。
XATransactionDataSource getConnection() 流程
我们都知道想要执行SQL语句,必须要获取到数据库的connection。让我们再回到 XAShardingTransactionManager.getConnection()
最后会调用到org.apache.shardingsphere.transaction.xa.jta.datasourceXATransactionDataSource.getConnection()
- 首先第一步很关心,尤其是对shardingsphere来说,因为在一个事务里面,会有多个SQL语句,打到相同的数据库,所以对相同的数据库,必须获取同一个XAConnection,这样才能进行XA事务的提交与回滚。
- 我们接下来关心
transaction.enlistResource(new SingleXAResource(resourceName, xaConnection.getXAResource()));
, 会进入com.atomikos.icatch.jta.TransactionImp.enlistResource()
, 代码太长,截取一部分。
- 我们直接看
restx.resume();
- 哦多尅,看见了吗,各位,看见了
this.xaresource.start(this.xid, flag);
了吗????,我们进去,假设我们使用的Mysql数据库:
- 组装
XA start Xid
SQL语句,进行执行。
到这里,我们总结下,在获取数据库连接的时候,我们执行了XA协议接口中的 XA start xid
atomikos事务commit流程
好了,上面我们已经开启了事务,现在我们来分析下事务commit流程,我们再把视角切换回XAShardingTransactionManager.commit()
,最后我们会进入com.atomikos.icatch.imp.CompositeTransactionImp.commit()
方法:
- 我们关注
coordinator.terminate ( true );
- 首先会判断参与者的个数,这里我们可以理解为MySQL的database数量,如果只有一个,退化成一阶段,直接提交。
如果有多个,则走标准的XA二阶段提交流程。
- 我们来看
prepare ();
流程,最后会走到com.atomikos.icatch.imp.PrepareMessage.send()
--->com.atomikos.datasource.xa.XAResourceTransaction.prepare()
- 终于,我们看到了这么一句
ret = this.xaresource.prepare(this.xid);
但是等等,我们之前不是说了,XA start xid
以后要先XA end xid
吗?答案就在suspend();
里面。
到了这里,我们已经执行了 XA start xid -> XA end xid --> XA prepare xid, 接下来就是最后一步 commit
- 我们再回到
terminate(false)
方法,来看 commit()流程。其实和 prepare流程一样,最后会走到com.atomikos.datasource.xa.XAResourceTransaction.commit()
。commit执行完,数据提交
思考:这里的参与者提交是在一个循环里面,一个一个提交的,如果之前的提交了,后面的参与者提交的时候,挂了,就会造成数据的不一致性。
Atomikos rollback() 流程
上面我们已经分析了commit流程,其实rollback流程和commit流程一样,我们在把目光切换回 org.apache.shardingsphere.transaction.xa.XAShardingTransactionManager.rollback()
,最后会执行到com.atomikos.icatch.imp.CompositeTransactionImp.rollback()
。
- 重点关注
coordinator.terminate ( false );
,这个和 commit流程是一样的,只不过在 commit流程里面,参数传的是true。
- 我们重点关注
rollback()
,最后会走到com.atomikos.datasource.xa.XAResourceTransaction.rollback()
。
- 先在
supend()
方法里面执行了XA end xid
语句, 接下来执行this.xaresource.rollback(this.xid);
进行数据的回滚。
Atomikos 事务恢复流程
在说,事务恢复流程之前,我们来讨论下,会啥会出现事务恢复?,XA 2阶段提交协议不是强一致性的吗?。要解答这个问题,我们就要来看看XA 二阶段协议有什么问题?
问题一 :单点故障
由于协调者的重要性,一旦协调者TM发生故障。参与者RM会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)
问题二 :数据不一致
数据不一致。在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据不一致性的现象。
如何解决?
解决的方案简单,就是我们在事务的操作的每一步,我们都需要对事务状态的日志进行人为的记录,我们可以把日志记录存储在我们想存储的地方,可以是本地存储,也可以中心化的存储。atomikos的开源版本,我们之前也分析了,它是使用内存 + file的方式,存储在本地,这样的话,如果在一个集群系统里面,如果有节点宕机,日志又存储在本地,所以事务不能及时的恢复(需要重启服务)。
Atomikos 多场景下事务恢复。
Atomikos 提供了二种方式,来应对不同场景下的异常情况。
- 场景一:服务节点不宕机,因为其他的原因,产生需要事务恢复的情况。这个时候才要定时任务进行恢复。
具体的代码com.atomikos.icatch.imp.TransactionServiceImp.init()
方法,会初始化一个定时任务,进行事务的恢复。
- 最终会进入
com.atomikos.datasource.xa.XATransactionalResource.recover()
方法。
- 场景二: 当服务节点宕机重启动过程中进行事务的恢复。具体实现在
com.atomikos.datasource.xa.XATransactionalResource.setRecoveryService()
方法里面
com.atomikos.datasource.xa.XATransactionalResource.recover() 流程详解。
- 我们来看一下如何根据
XA recovery 协议获取RM端存储的xid
。进入方法retrievePreparedXidsFromXaResource(xaResource)
, 最后进入com.atomikos.datasource.xa.RecoveryScan.recoverXids()
方法。
- 我们重点关注
xidsFromLastScan = xaResource.recover(flags);
这个方法,如果我们使用MySQL,那么久会进入 MysqlXAConnection.recover()方法。执行XA recovery xid
语句来获取 xid
-
这里要注意如果Mysql的版本 <5.7.7 ,则不会有任何数据,在以后的版本中Mysql进行了修复,因此如果我们想要使用MySQL充当RM,版本必须 >= 5.7.7
,原因是:
MySQL 5.6版本在客户端退出的时候,自动把已经prepare的事务回滚了,那么MySQL为什么要这样做?这主要取决于MySQL的内部实现,MySQL 5.7以前的版本,对于prepare的事务,MySQL是不会记录binlog的(官方说是减少fsync,起到了优化的作用)。只有当分布式事务提交的时候才会把前面的操作写入binlog信息,所以对于binlog来说,分布式事务与普通的事务没有区别,而prepare以前的操作信息都保存在连接的IO_CACHE中,如果这个时候客户端退出了,以前的binlog信息都会被丢失,再次重连后允许提交的话,会造成Binlog丢失,从而造成主从数据的不一致,所以官方在客户端退出的时候直接把已经prepare的事务都回滚了!
- 回到主线,假设我们获取到需要进行事务恢复的XID,再从自己记录的事务日志里面获取XID,如果前者包含在后者之中,则进行commit,否则进行rollback.
- replayCommit 方法如下:
- attemptPresumedAbort(xid, xaResource); 方法如下:
文章到此,已经写的很长很多了,我们分析了ShardingSphere对于XA方案,提供了一套SPI解决方案,对Atomikos进行了整合,也分析了Atomikos初始化流程,开始事务流程,获取连接流程,提交事务流程,回滚事务流程,事务恢复流程。
希望对大家理解XA的原理有所帮助。
作者介绍:肖宇,Apache ShardingSphere Committer,开源hmily分布式事务框架作者,
开源soul网关作者,热爱开源,追求写优雅代码。目前就职于京东数科,参与ShardingSphere的开源建设,以及分布式数据库的研发工作。
