一、使用
1.1 增加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
1.2 配置 application.yml
spring:
datasource:
type: com.alibaba.druid.pool.xa.DruidXADataSource
### 主要就是更换链接池的 type 为 DruidXADataSource
jta:
log-dir: classpath:tx-logs
transaction-manager-id: txManager
1.3 更改 DataSource 配置
@Bean
@Primary
public DataSource dataSource() {
// 使用 DruidXADataSource
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);
// 将 DruidXADataSource 配置给 AtomikosDataSourceBean
AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();
atomikosDataSourceBean.setXaDataSource(datasource);
return datasource;
}
@Bean(name = "xatx")
@Primary
public JtaTransactionManager jtaTransactionManager() {
UserTransactionManager userTransactionManager = new UserTransactionManager();
UserTransaction userTransaction = new UserTransactionImp();
return new JtaTransactionManager(userTransaction, userTransactionManager);
}
1.4 更改 @Transaction 注解
@Transactional(transactionManager = "xatx" , rollbackFor = Exception.class)
// transactionManager = "xatx" 属性为第三步创建的 Bean 对象
二、基本原理
2.1 分布式事务创建
这个和 Spring 传统事务相比来说,Spring 的事务通过的是 JpaTransactionManager 进行管理,而这套逻辑采用的是 JtaTransactionManager 进行管理。通过调用 Atomikos 框架实现相关逻辑。
这个的核心就是 CompositeTransaction 对象,这个对象基本就可以说是一个分布式事务,它是从一个 map 中进行获取,这个 map 的 key 是当前线程,value 就是 CompositeTransaction 对象,如果 map 集合中没有的话,就会进行创建,并将其加入到 map 集合中。这里要注意的是,这个 map 是 hashMap , 所以在获取的时候,加入了 synchronized 机制。
private CompositeTransactionImp getCurrentTx() {
Thread thread = Thread.currentThread();
synchronized(this.threadtotxmap_) {
// 获取 CompositeTransactionImp 对象
Stack txs = (Stack)this.threadtotxmap_.get(thread);
return txs == null ? null : (CompositeTransactionImp)txs.peek();
}
}
这样,获取到线程对应的 CompositeTransactionImp 对象,我们的 SQL 都是由这个线程去进行执行的,这个线程对应的 CompositeTransactionImp 也就代表了执行 SQL 所在的全部库,多个库的事务统一由 CompositeTransactionImp 对象进行管理,同时会为这个事务生成一个事务 id。
2.2 Atomikos 包装 DataSource
public DataSource dataSource() {
DruidXADataSource datasource = new DruidXADataSource();
datasource.setUrl(this.dbUrl);
datasource.setUsername(username);
datasource.setPassword(password);
datasource.setDriverClassName(driverClassName);
... ...
AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();
atomikosDataSourceBean.setXaDataSource(datasource);
return datasource;
}
我们通过配置文件设置链接池的各种参数之后,会通过创建 DruidXADataSource 对象,获取 Connection 链接,这里我们将链接交给了 AtomikosDataSourceBean ,由 AtomikosDataSourceBean 进行了一定的包装。
核心类就是 AtomikosDataSourceBean 的父类 AbstractDataSourceBean ,通过 getConnection() 去获取链接对象,主要就是通过 connectionPool 链接池去获取对象,这个链接池是通过 init() 方法进行初始化。
public Connection getConnection ( HeuristicMessage msg ) throws SQLException
{
if ( LOGGER.isInfoEnabled() ) LOGGER.logInfo ( this + ": getConnection ( " + msg + " )..." );
Connection connection = null;
// 初始化
init();
try {
connection = (Connection) connectionPool.borrowConnection ( msg );
} catch (CreateConnectionException ex) {
throwAtomikosSQLException("Failed to grow the connection pool", ex);
} catch (PoolExhaustedException e) {
throwAtomikosSQLException ("Connection pool exhausted - try increasing 'maxPoolSize' and/or 'borrowConnectionTimeout' on the DataSourceBean.");
} catch (ConnectionPoolException e) {
throwAtomikosSQLException("Error borrowing connection", e );
}
if ( LOGGER.isDebugEnabled() ) LOGGER.logDebug ( this + ": returning " + connection );
return connection;
}
protected com.atomikos.datasource.pool.ConnectionFactory doInit() throws Exception
{
... ...
// 这里的 xaDataSource 就是我们创建好的 DruidXADataSource
xaDataSource = (XADataSource) driver;
xaDataSource.setLoginTimeout ( getLoginTimeout() );
xaDataSource.setLogWriter ( getLogWriter() );
PropertyUtils.setProperties(xaDataSource, xaProperties );
}
JdbcTransactionalResource tr = new JdbcTransactionalResource(getUniqueResourceName() , xaDataSource);
// 这就是最后创建好的 ConnectionFactory
com.atomikos.datasource.pool.ConnectionFactory cf = new com.atomikos.jdbc.AtomikosXAConnectionFactory(xaDataSource, tr, this);
Configuration.addResource ( tr );
return cf;
}
将 DruidXADataSource 创建好链接池之后,就是获取到链接,对链接进行包装了,主要就是创建了一个动态代理对象,AtomikosConnectionProxy 这个对象的 invoke() 方法,就是所有逻辑的实现,这里只是包装,具体逻辑实现,放在下面标题。
public static Reapable newInstance ( Connection c , SessionHandleState sessionHandleState , HeuristicMessage hmsg )
{
Reapable ret = null;
AtomikosConnectionProxy proxy = new AtomikosConnectionProxy(c, sessionHandleState , hmsg );
Set<Class> interfaces = PropertyUtils.getAllImplementedInterfaces ( c.getClass() );
interfaces.add ( Reapable.class );
//see case 24532
interfaces.add ( DynamicProxy.class );
Class[] interfaceClasses = ( Class[] ) interfaces.toArray ( new Class[0] );
Set<Class> minimumSetOfInterfaces = new HashSet<Class>();
minimumSetOfInterfaces.add ( Reapable.class );
minimumSetOfInterfaces.add ( DynamicProxy.class );
minimumSetOfInterfaces.add ( java.sql.Connection.class );
Class[] minimumSetOfInterfaceClasses = ( Class[] ) minimumSetOfInterfaces.toArray( new Class[0] );
List<ClassLoader> classLoaders = new ArrayList<ClassLoader>();
classLoaders.add ( Thread.currentThread().getContextClassLoader() );
classLoaders.add ( c.getClass().getClassLoader() );
classLoaders.add ( AtomikosConnectionProxy.class.getClassLoader() );
ret = ( Reapable ) ClassLoadingHelper.newProxyInstance ( classLoaders , minimumSetOfInterfaceClasses , interfaceClasses , proxy );
return ret;
}
2.3 Start 指令和 SQL 准备
其实 Mybatis 框架要执行的时候,会通过 mapper 组件,通过链接池获取到链接对象,对 SQL 语句进行执行,在这里的话,就是获取到经过包装之后的 AtomikosConnectionsProxy 动态代理对象,核心的逻辑都是由这个动态代理对象的 invoke() 方法来完成 。
这里也是有 XAConnection ,XAResource 这些概念,通过 之前包装的 ConnectionFactory 获取到 CompositeTransactionImp 对象,这个对象就是全局的事务对象 。
当我们要执行 SQL 的时候,会被动态代理对象拦截,通过Druid 链接池的信息,构建好 XAConnection ,XAResouce 对象,这里 Atomikos 框架对 XAResource 进行了封装(XAResourceTransaction),XAResourceTransaction 这个代表的就是分布式事务的一个子事务,这个库的 SQL 第一次执行的时候,就会创建这样的一个子事务并将其加入到 CompositeTransactionImp 全局事务中,然后通过这个子事务的 XAResource 开始 START 指令。
后续的话,就是会去执行 prepareStatement 命令,这个就是去执行我们要执行的 SQL 语句,这个就是通过调用 jdbc 原生的进行执行。
这样也就完成了 START 指令 和 SQL 语句的准备。
2.4 End 指令
end 指令说到底也是通过子事务的 XAResource 对象进行发送的,基本和上面一样,最终就是 xaresource_.end(xid_,flag)
2.5 终止
这里面逻辑实在是太深了,大体就记一下,主要就还是通过 xaresource.commit 执行 commit , 且后续会通过 全局事务 CompositeTransaction 获取到所有的子事务,对子事务进行遍历,每个子事务都执行 prepare() 方法,返回一个执行结果,到这里为止,2PC 的第一阶段(Prepare)就完事了。
主要就是在终止分布式事务中,执行 prepare 等待返回结果,根据结果判断是执行 commit 还是 rollback
protected void terminate ( boolean commit ) throws HeurRollbackException,
HeurMixedException, SysException, java.lang.SecurityException,
HeurCommitException, HeurHazardException, RollbackException,
IllegalStateException
{
synchronized ( fsm_ ) {
if ( commit ) {
if ( participants_.size () <= 1 ) {
commit ( true );
} else {
// 这里就是去执行 prepare() 方法
int prepareResult = prepare ();
// make sure to only do commit if NOT read only
if ( prepareResult != Participant.READ_ONLY )
commit ( false ); // 最终根据判断执行提交或者回滚
}
} else {
rollback ();
}
}
}
如果是 commit 的话,就是对分布式中所有的子事务进行遍历,通过 propagator_.submitPropagationMessage ( cm ); 去触发整个 commit 的过程。
Enumeration<Participant> enumm = participants.elements ();
while ( enumm.hasMoreElements () ) {
Participant p = enumm.nextElement ();
if ( !readOnlyTable_.containsKey ( p ) ) {
CommitMessage cm = new CommitMessage ( p, commitresult,
onePhase );
// if onephase: set cascadelist anyway, because if the
// participant is a REMOTE one, then it might have
// multiple participants that are not visible here!
if ( onePhase && cascadeList_ != null ) { // null for OTS
Integer sibnum = (Integer) cascadeList_.get ( p );
if ( sibnum != null ) // null for local participant!
p.setGlobalSiblingCount ( sibnum.intValue () );
p.setCascadeList ( cascadeList_ );
}
//
propagator_.submitPropagationMessage ( cm );
}java
} // while
最后的 commit 还是通过每个 RM 对应的 XAResource 去执行 commit 操作,进行事务提交,
xaresource_.commit ( xid_, onePhase );
try {
// refresh xaresource for MQSeries: seems to close XAResource after suspend???
testOrRefreshXAResourceFor2PC ();
... ...
// 这就是提交核心
xaresource_.commit ( xid_, onePhase );
} catch ( XAException xaerr ) {
String msg = interpretErrorCode ( resourcename_ , "commit" , xid_ , xaerr.errorCode );
LOGGER.logWarning ( msg , xaerr );
}
如果结果不正常的话,是不会走 commit 的,会进入 rollback 逻辑,进行整体所有子库的回滚操作
Enumeration enumm = participants.elements ();
while ( enumm.hasMoreElements () ) {
Participant p = (Participant) enumm.nextElement ();
if ( !readOnlyTable_.containsKey ( p ) ) {
RollbackMessage rm = new RollbackMessage ( p,
rollbackresult, indoubt );
// 发送回滚消息
propagator_.submitPropagationMessage ( rm );
}
}
rollback 逻辑和 commit 基本是一致的,也是通过 全局事务获取到全局事务中所有的子事务,对子事务进行遍历,通过 propagator_.submitPropagationMessage ( rm ); 发送回滚执行,最终还是通过子事务对应的 XAResource 去执行 rollback 操作 :xaresource_.rollback ( xid_ );
try {
if ( state_.equals ( TxState.ACTIVE ) ) { // first suspend xid
suspend ();
}
// refresh xaresource for MQSeries: seems to close XAResource after suspend???
testOrRefreshXAResourceFor2PC ();
if(LOGGER.isInfoEnabled()){
LOGGER.logInfo("XAResource.rollback ( " + xidToHexString + " ) "
+ "on resource " + resourcename_
+ " represented by XAResource instance " + xaresource_);
}
xaresource_.rollback ( xid_ );
} catch ( ResourceException resErr ) {
// failure of suspend
errors.push ( resErr );
throw new SysException ( "Error in rollback: "
+ resErr.getMessage (), errors );