JTA + Atomikos 源码剖析

2,128 阅读5分钟

一、使用

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。

JTA.png

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;
    }

AtomikosConnectionProxy.png

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 语句的准备。

START 和 SQL.png

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 );

三、总结

Atomikos.png