04_基于JTA+Druid多数据源+Atomikos实现单实例多数据源的事务控制

一. JTA+Druid+Atomickos的使用方式

1.1 引入依赖

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>

1.2 增加配置

在application.yml中增加如下配置,db1、db2指的是我们配置的多数据源。

db1:
  datasource:
    type: com.alibaba.druid.pool.xa.DruidXADataSource
db2:
  datasource:
    type: com.alibaba.druid.pool.xa.DruidXADataSource
jta:
  log-dir: classpath:tx-logs
  transaction-manager-id: txManager

1.3 druid数据源配置

  1. 去掉单库使用的DatasourceTransactionManager
  2. 使用JtaTransactionManager,参考下述代码
@Bean(name = "myJtm")
@Primary
public JtaTransactionManager activityTransactionManager() {
	UserTransactionManager userTransactionManager = new UserTransactionManager();
	UserTransaction userTransaction = new UserTransactionImp();
	return new JtaTransactionManager(userTransaction, userTransactionManager);
}

我们可能会配置多个数据源,不过只需要在任意一个数据源中,加上对事务管理器(JtaTransactionManager)的配置就可以了。

JtaTransactionManager来自: org.springframework.transaction.jta.JtaTransactionManager
UserTransactionManager来自: com.atomikos.icatch.jta.UserTransactionManager

  1. 对于Datasource的创建,参考如下代码
@Primary
@Bean(name = "db1Datasource")
public DataSource activityDatasource() {
	DruidXADataSource datasource = new DruidXADataSource();
	datasource.setUrl(this.dbUrl);
	datasource.setUsername(username);
	datasource.setPassword(password);
	datasource.setDriverClassName(driverClassName);
	datasource.setInitialSize(initialSize);
	datasource.setMinIdle(minIdle);
	datasource.setMaxActive(maxActive);
	datasource.setMaxWait(maxWait);
	datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
	datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
	datasource.setValidationQuery(validationQuery);
	datasource.setTestWhileIdle(testWhileIdle);
	datasource.setTestOnBorrow(testOnBorrow);
	datasource.setTestOnReturn(testOnReturn);
	datasource.setPoolPreparedStatements(poolPreparedStatements);
	datasource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize);

	try {
		datasource.setFilters(filters);
	} catch (SQLException e) {
		e.printStackTrace();
	}

	datasource.setConnectionProperties(connectionProperties);

	AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();
	atomikosDataSourceBean.setXaDataSource(datasource);

	return atomikosDataSourceBean;
}

1.4 增加事务注解

@Transactional(transactionManager = "myJtm", rollbackFor = Exception.class)

transactionManager 填写我们刚刚设置的事务管理器的名称。

二. JTA+Atomikos事务的源码分析

2.1 创建分布式事务

从理论上来说,无非就是JTA这套分布式事务API,借助Atomikos分布式事务框架,做出来了一个TM(事务管理器)。

研究源码的入口仍然是TransactionInterceptor #invoke(),首当其冲,我们要看分布式事务是如何创建出来的,所以视线聚焦到TransactionAspectSupport #createTransactionIfNecessary()。

之前做单库事务时,我们使用的是DatasourceTransactionManager,现在使用的是在druid数据源中配置的JtaTransactionManager。

DataSourceTransactionManager由spring-jdbc.jar提供,它实现了PlatformTransactionManager接口,不支持DTP模型和XA规范,只能实现单实例单数据库的事务控制。

JtaTransactionManager同样是由spring-tx.jar提供,它实现了PlatformTransactionManager接口,支持DTP模型和XA规范,可以实现单实例多数据库的事务控制。

其实我根本就不想把Atomickos框架称作分布式事务的解决方案,这东西充其量只是解决单实例多数据库的事务控制,横跨了多个数据库而已,请问"分布式"这几个字,体现在哪里了?
在这里插入图片描述
接着会走到JtaTransactionManager的doBegin()方法(之前单库事务走的是JpaTransactionManager),执行JtaTransactionManager #doJtaBegin(),然后执行UserTransactionImpl #begin()。这里与单库事务有所不同,单库事务底层依赖的是hibernate提供的TransactionImpl,而JTA走的是由Atomikos提供的UserTransactionImp。

接着走到了TransactionManagerImpl #begin(),我们发现,后续所有的操作都是基于CompositeTransaction来做的。CompositeTransaction就是我们要找的分布式事务。

JTA搞了一个Map结构,为每个线程单独创建了一套栈,栈中显然可以存放多个CompositeTransaction对象。

那么问题来了,JTA为什么要为每个线程分别维护一套栈呢?

这是因为,CompositeTransaction代表着一个分布式事务。对于同一个线程而言,在一个请求中,可能会涉及到多个分布式事务,这里就是把一次请求中的多个分布式事务,压入了JTA维护的“栈”中。

private Map<Thread, Stack<CompositeTransaction>> threadtotxmap_ = null;

第一次接收到请求时,当前线程对应的栈肯定是空的,因此从栈中取出来的CompositeTransation一定是null,所以需要初始化。CompositeTransation的初始化工作是由BaseTransactionManager完成的,具体的初始化过程如下:

首先,BaseTransactionManager会从Map中取出当前线程对应的栈,接着从栈中弹出位于栈顶的CompositeTransation,接着,如果发现弹出的CompositeTransation是null,则会创建一个CompositeTransation,并把它压入栈,最后把栈存入Map中。

2.2 创建Datasource和Connection

2.1节的代码走完,也就是CompositeTransaction创建完毕后,我感到非常疑惑,为什么没有看到获取Connection,设置一些参数,比如关闭自动提交等代码呢?其实,使用了Atomikos框架的JTA Api与原生底层通过Hibernate实现的方式略有不同。

遗憾的是,我自己尝试直接跟踪TransactionAspectSupport的invokeWithinTransaction()方法,没有找到Datasource和Connection的创建过程。

既然找不到代码,那就开始推测吧。我们现在是在做横跨多个数据库的分布式事务,操作数据库的代码肯定是XXXMapper,在执行Mapper中的接口时,Mybatis会为我们向SqlSessionFactory申请Datasource,Datasource本身不具备把指令传输到数据库的能力,它一定会继续找数据库连接池,获取一个连接(Connection)。

还记得在1.3节,我们定义的AtomikosDataSourceBean,它就是基于Atomickos框架实现的Datasource。所以,接下来,我们就来看看AtomikosDataSourceBean中是否有获取数据库连接的代码。

Spring Boot在与Atomickos框架整合时,自己搞了一个AtomikosDataSourceBean,看上去仅仅只是适配了Spring框架,增加了Spring Bean生命周期必须的一些方法。它的真正的方法全都定义在com.atomikos.jdbc.AtomikosDataSourceBean中,结合1.3小节创建Datasource的过程,很容易发现,AtomikosDataSourceBean底层依赖的是DruidXADataSource。

那么现在就看看AtomikosDataSourceBean到底是怎样获取Connection的,AbstractDataSourceBean是AtomikosDataSourceBean的父类。
在这里插入图片描述
红色框的connectionPool是Atomikos提供的连接池,请求走到了ConnectionPool #retrieveFirstAvailableConnection(),光看方法名字,感觉是用于获取第一个可以使用的连接。

继续往下走,请求走到了AtomikosConnectionProxy #newInstance()。
在这里插入图片描述
上图中,Connection c是DruidXADatasource提供的数据库连接,它的类型是java.sql.Connection。但是Atomikos框架显然不打算直接使用这个Connection,它把Connection封装到了AtomikosConnectionProxy中,这个AtomikosConnectionProxy非常特殊,它继承了InvocationHandler,我看到InvocationHandler,条件反射,立刻想到JDK动态代理,果不其然,上图的第二个红色框,创建了一个动态代理,代理的接口中包括java.sql.Connection。

值得注意的是,这个动态代理对象的类型是ConnectionProxyImpl,它是druid提供的,这就说明,druid故意留了实现方式,能让其它的框架对自己提供的Connection进行功能增加,实现druid自己无法实现的功能,比如说Atomikos就提供了实现分布式事务的功能。而druid本身专注于实现数据库连接池、数据库连接等功能就够了。

总结:

Java的JTA Api使用Atomikos框架实现分布式事务时,首先通过DruidXADatasource数据源,在Druid连接池中,获取可以使用的数据库连接,接着基于数据库连接,做了一个动态代理对象(Connection),由Atomikos框架进行功能增强,实现分布式事务的功能。

至于增加了哪些功能,它们是怎样实现分布式事务的呢?这就要看2.3节了。

2.3 发送XA START

我之前写过原生的XA分布式事务,它的执行过程无非就是:

  1. 针对事务,生成txid
  2. 使用XA start txid和XA end txid指令,包裹待执行的业务代码。执行start和end命令的是XAResource。
  3. 分别向涉及到的数据库发送XA prepare txid,并获取返回结果
  4. 如果所有的数据库都返回OK,则再次向所有的数据库发送XA commit txid,提交事务。

通过2.2节的分析,我们已经生成了分布式事务,那么XA start txid和XA end txid指令是在哪里执行的呢?

首先,请求接口,执行sql时,请求会被AtomikosConnectionProxy的invoke()拦截。

第一次拦截时,需要执行的方法是getAutoCommit(),返回的结果是true。

第二次拦截时,需要执行的方法是prepareStatement(),根据之前学习原生的数据库连接代码的经验,看到prepareStatment(),立刻想到这里一定是要填写需要执行的sql了。请求进入AtomikosConnectionProxy #enlist()。
在这里插入图片描述
继续向红色框代码内跟踪,我发现核心代码在NotInBranchStateHandler的checkEnlistBeforeUse()内。
在这里插入图片描述
红色框第二个参数是XAResource,这不就是用来执行Start或者End操作的类么。

在这里插入图片描述
在BranchEnlistedStateHandler中,我发现了两个变量:

  1. CompositeTransaction 代表着一个分布式事务
  2. XAResourceTransaction 代表着一个分布式事务中,对于一个数据库操作的子事务。

之前说了,一个分布式事务中,可能会涉及对多个数据库的操作,那么每个数据库的操作,其实分别会创建一个子事务,而这些子事务会被加入到所属的分布式事务中(每个子事务拥有自己的事务id)。CompositeTransaction 存储这些子事务时,使用了Vector数据结构,代码位置在: CoordinatorImp

private Vector<Participant> participants_ = new Vector<Participant>();

继续看branch.resume()方法,这个方法内部执行了XAResource的start(),XAResource的实现类是MysqlXAConnection,可以看看它的start()方法的源码:
在这里插入图片描述
这里拼了一个指令: XA START xxxxxxxxxxxxxxxxxx,并把指令发送给了Mysql,其中xxxxxxx指的是子事务的id。

接着,回到AtomikosConnectionProxy的invoke()方法中,执行method.invoke(delegate, args);通过断点,很容易得知:

  1. method是Connection.prepareStatement()
  2. delegate是Connection对象
  3. args就是本次需要执行的sql语句以及入参

这里就是调用原生的Connection,执行prepareStatement(),并放入一个List中。如果本次子事务中,对数据库的操作涉及到多条sql,那么每次sql执行,都会创建一个PrepareStatement,并放入List。所以,这里List最终存储了本次子事务需要执行的所有sql。

在同一个子事务中,第二次操作同一个数据库,并执行sql时,代表那个数据库的子事务(XAResourceTransaction就不会被创建了),所以只有在第一次创建XAResourceTransaction时,才会执行XA START指令。这是因为AtomikosConnectionProxy #invoke() 会执行isEnlistedInGlobalTransaction(),判断本次操作的数据库有没有对应的子事务,有没有加入到全局事务中,如果没有,则创建子事务,并执行XA START指令,否则就不会创建。

在对所有的数据库都执行了XA START指令,并且依次执行了prepareStatement(),收集了需要执行的SQL后,下一步肯定是对每个数据库都执行XA END指令,并发送XA PREPARE指令,完成2PC的prepare阶段。

总结:

Java对数据库的操作,底层实际上是由java.sql.Connection来完成的。Atomikos框架为Connection等接口创建了一个动态代理对象,对Connection的请求全部转发到AtomikosConnectionProxy的invoke()方法中。初次接收到某个数据库的sql执行请求后,Atomikos会为本次数据库操作创建一个子事务,执行XA START指令,并且把本次待执行的sql装入PrepareStatement中,放入List集合,后续再次对这个数据库进行操作时,直接使用已经创建好的子事务,直接填入sql即可。

2.4 发送XA END指令

回到TransactionInterceptor #invoke(),在经典的TransactionAspectSupport #invokeWithinTransaction()中,我们已经创建了分布式事务,执行了业务代码,并且为底层对数据库的操作分别创建了prepareStatement,存入子事务中。

再次复习spring提供的事务拦截器:
在这里插入图片描述由于此时尚未执行sql,所以这里有两种场景:

  1. 业务代码本身执行正常,准备执行TransactionAspectSupport的commitTransactionAfterReturning()。
  2. 业务代码本身执行报错,准备执行completeTransactionAfterThrowing()。

无论是哪种场景,都会触发XAResource.end,只不过第一种场景最后执行的是XAResource.commit,而第二种场景XAResource.rollback()。我们这里就以第一种场景为例,来看看XAResource.end指令到底是如何发出的。

首先,AbstractPlatformTransactionManager的processCommit()方法会被调用。

这个方法内执行了triggerBeforeCompletion(status); 它会借助Connection执行close()操作。

既然是对Connection执行操作,那么想都不用想,一定会被AtomikosConnectionProxy拦截下来,并交给它的invoke()。

这里会经历一大坨方法,不过实际上对我们有帮助的,只有以下几句:

  1. BranchEnlistedStateHandler #sessionClosed() 调用了BranchEndedStateHandler的构造函数,这里把本次请求数据库的子事务作为入参传了进来。
  2. 子事务XAResourceTransaction执行了suspend()方法,这个方法内的核心代码如下,其中,xaresource_是MysqlXAConnection。
xaresource_.end ( xid_, XAResource.TMSUCCESS );

接下来,就会执行JtaTransactionManager的doCommit()来提交事务了。

2.5 发送XA COMMIT指令

入口仍然是TransactionAspectSupport的commitTransactionAfterReturning(),在执行完AbstractPlatformTransactionManager的triggerBeforeCompletion()之后,即将执行JtaTransactionManager的doCommit()方法,这里会执行UserTransactionImp #commit(),接着执行CompositeTransactionImp的commit()。
在这里插入图片描述
由于我们看到了CompositeTransaction的字眼,所以可以断定,现在准备提交的是整个分布式事务,而不仅仅是某个子事务。

接着往下阅读,发现CoordinatorImp的terminate(),这个方法看上去太像XA的2PC协议了。
在这里插入图片描述
从prepare(),到commit(),再到rollback(),2PC中该有的环节,它应有尽有。

看看prepare(),代码跟踪到ActiveStateHandler的prepare()方法,这个方法中就是在遍历当前分布式事务内的所有子事务,底层执行了XAResourceTransaction的prepare()方法,也就是说,对着每一个数据库,分别发送XA prepare()。

2.6 发送XA ROLLBACK指令

从2.4节中,我们知道了ActiveStateHandler的prepare()方法会遍历当前分布式事务内所有的子事务,分别发送XA prepare指令。接着,prepare()方法会收集每一个子事务执行XA prepare指令的返回结果,封装成Apply对象,并放入PrepareResult对象中。

由于每个子事务的背后,其实都是一个数据库,考虑到它们的事务相互隔离,并且执行指令、返回执行结果的速度有所不同,所以,Atomikos框架为每一个子事务都搞了一个线程来发送指令,循环线程发送指令后,主线程就会被卡住,当且仅当所有的线程返回结果,主线程才会继续执行下去。此时,主线程会判断本次分布式事务内的所有子事务的执行结果是否全部返回了正常。对应的代码如下:

在这里插入图片描述
红色框,当且仅当所有的子事务的XA prepare指令都执行成功了,voteOK才会为true。

如果不是所有的库都返回OK,则Atomikos框架会遍历每一个Participant(执行成功的子事务),然后对每个库分别执行XA ROLLBACK指令(利用的仍然是XAResource的rollback())。

三. 总结

JTA仅仅是Java对处理事务提供的一套通用的Api,连完整的解决方案都谈不上,它定义了处理事务的几个步骤:

  1. 创建事务
  2. 执行业务逻辑
  3. 看看业务逻辑是否报错,如果报错,则回滚事务
  4. 如果业务逻辑不报错,则提交事务
  5. 最后做一些事务的清理工作

Spring框架在使用JTA时,为其整合了hibernate,使用了比如hibernate-entitymanager,hibernate-core之类的类库,最起码提供了一套对JTA Api的底层实现,能满足单系统,单数据库的事务管理。

现在我们遇到了单系统,跨多数据库的分布式事务,由于一个方法内,请求了多个数据库,每个数据库本身的事务相互隔离,所以使用JTA+hibernate肯定无法解决这种场景的。

那么怎么办呢?这个时候,我们想到了使用X/open组织提出的DTP模型,我们也可以搞一些AP、RM、TM、CRM对象出来,遵守XA接口规范,并且基于2PC或者3PC的协议,实现整套分布式事务的解决方案。当然了,我们的确可以自己去实现,难度倒是次要的,只不过要花时间啊,好在已经有现成的框架供我们使用,那就是Atomikos框架。

Atomikos框架,就是在JTA的基础上,代替hibernate,实现了TM,最终实现了整套适用于单应用、多数据库的分布式事务场景。

那么Atomikos框架是怎么实现TM的呢?其实,TM的核心概念就是基于2PC协议,或者3PC协议,实现XA接口规范,让AP与RM通信。换句话说,只要Atomikos框架能实现这套执行流程(比如包括prepare阶段、commit阶段),那就相当于实现了TM。

Atomikos框架认为,反正目标方法最终肯定是要向数据库发送请求的,而且底层走的都是java.sql.Connection,所以干脆为Connection做了一个动态代理对象,所有对Connection的操作都会转发到这个动态代理对象上,然后在部分指令上,增加了一些指令(比如XA start txid),发送给数据库。

我们知道,一条发送到应用服务(AP)的请求,一定会由一条线程来处理(比如Tomcat线程)。一个请求对应着一个方法,而这个方法内可能会对应若干个子方法,假设子方法最终会执行sql,那么不同的子方法可能会在不同的数据库中执行sql。

针对这些细节,Atomikos框架提出了几个概念:

  1. 全局事务
    Atomikos框架把一次外部请求产生的事务,笼统称之为一个"全局事务"。
  2. 分布式事务
    比如说我们使用了PROPAGATION_REQUIRED_NEW这种事务传播行为,那么一次请求可能会经过多个方法,每个方法都会开启自己的事务,我们把它称之为分布式事务。(CompositeTransaction)
  3. 子事务
    一个分布式事务对应着一个方法,而这个方法内,可能会有多个子方法,请求不同的数据库,我们知道,数据库各自的事务是相互独立的,所以把每个子方法产生的事务称为一个子事务。(XAResourceTransaction)

有了这些概念,那么理解起来就非常简单了。

我们知道,Atomikos框架无非就是JTA Api的补充,他主要在以下几个阶段增加了功能:

  1. 创建分布式事务。
    对每个需要开启独立事务的方法,分别创建分布式事务(CompositeTransaction)。之前分析过了,一个外部请求可能会涉及到多个分布式事务,所以Atomikos框架使用Map<Thread, Stack<CompositeTransaction>>这种数据结构来存储。
  2. 执行业务方法。
    Atomikos框架会把对一个数据库的初次请求,做一个子事务(XAResourceTransaction)出来,并向数据库发送一个XA START指令。接着,每次对数据库的请求都会被做成一个prepareStatement,包裹待执行的sql。考虑到对于同一个数据库可能会有多次sql操作,那么每次操作,都会产生一个prepareStatement,并放入子事务持有的一个List中(具体的数据结构忘了,大概就是一个集合吧)。这个子事务,也会被放入分布式事务当中,
  3. 提交分布式事务。
    假设现在已经执行完了方法中的所有代码,那么我们肯定是想提交全局事务,但是别忘了,全局事务包含了多个分布式事务,所以得分开提交。这里会遍历每个分布式事务包含的子事务,分别发送XA END,接着再遍历子事务,分别发送XA prepare,这个过程实际上就是在让数据库真正的执行sql了,只不过不commit而已。若所有的子事务的prepare全部执行成功,则再次遍历子事务,分别发送XA commit。只要有任意一个子事务的prepare执行失败,或者返回的不是OK,那么就遍历子事务,分别发送XA rollback。

这个Connection,其实也是Durid提供的。首先是DruidXADatasource中,会搞一个DruidPoolConnection,这里面会存放XAConnection,其实就是对java.sql.Connection进行了封装,接着会把XAConnection注入到MysqlXAConnection,创建出XAResource。前文中发送XA START、END、COMMIT等操作,都是由XAResource提供的方法。这里非常值得我注意,因为这就证实了,不是所有的数据库都遵循XA规范的,只有像Mysql这种适配了XA规范的数据库,才能执行XA START或者XA END等操作,否则你去找个"达梦数据库",能做XA事务吗?估计是不可以吧。

最后,我想用一句话来总结Atomikos框架:

Atomikos框架与JTA进行了整合,通过对Connection创建动态代理对象的方法,底层实现了XA 2PC或者3PC的细节,其实就是实现了一个TM。事务的大致处理流程仍然是由JTA的动态代理对象来引导,最终实现了单应用、横跨多数据库的分布式事务。

最核心的代码:

  1. JTA的动态代理对象: TransactionInterceptor invoke()
  2. Connection的动态代理对象: AtomikosConnectionProxy invoke()
  3. XAResource,实现了与Mysql进行通信。

画成图如下所示:
在这里插入图片描述

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值