分析 Mybatis-Plus 多数据源事务失效

Table Of Contents

项目中使用了 com.baomidou:dynamic-datasource 引入多数据源以及 Mybatis-Plus 作为 DAO 层开发,但是使用事务注解 @Transactional 的时候,发现即使抛出了异常,事务也没有回滚。

# Spring 的事务执行原理

首先我们需要使用 @EnableTransactionManagement 注解来开启事务,通过 @Import(TransactionManagementConfigurationSelector.class) 的语法注入事务切面管理的基础 Bean.

默认使用的是基于动态代理的注解驱动的事务管理,所有后续被代理的事务方法都会进入到事务通知器里的 TransactionInterceptor 拦截处理。

事务处理的核心逻辑在 TransactionAspectSupport#invokeWithinTransaction 里实现。

// 获取事务属性源,默认我们用的是 AnnotationTransactionAttributeSource
TransactionAttributeSource tas = getTransactionAttributeSource();
// 获取事务属性 TransactionAttribute (@Transactional 注解中的字段), 如果 NULL 则该方法为非事务方法
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
// 确定事务管理器 PlatformTransactionManager
final PlatformTransactionManager tm = determineTransactionManager(txAttr);
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

创建事务,绑定事务信息到当前线程

// 根据事务属性、事务管理器、连接点描述(用于监视和日志记录目的)创建一个事务信息 TransactionInfo 对象
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
// 如果 txAttr && tm 都不为空,则会根据指定的传播行为,返回当前活动的事务或创建新事务
// 隔离级别或超时等参数仅适用于新事务,因此在参与活动事务时将被忽略
TransactionStatus status = tm.getTransaction(txAttr);
// 把 status 赋值给 txInfo, 可以根据 txInfo.transactionStatus != null 判断是否创建了事务
txInfo.newTransactionStatus(status);
// 不管这里有没有创建事务,总是将 TransactionInfo 绑定到当前线程 transactionInfoHolder
// 将 transactionInfoHolder.get() 赋值给 txInfo.oldTransactionInfo, 然后 transactionInfoHolder.set(this)
txInfo.bindToThread();

接着通过环绕通知执行目标方法,最终进行事务的提交或者回滚操作

Object retVal;
try {
    // 实际 invocation::proceed lambda 表达式的执行
    // 将会继续向后调用链中的下一个拦截器,最后还会导致目标方法的调用(执行业务逻辑),然后会倒序返回
    retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
    // 如果创建了事务即 txInfo.transactionStatus != null, 在这里处理抛出的异常对象,完成事务,这一步可能提交或者回滚
    // 如果 txInfo.transactionAttribute.rollbackOn(ex) 为 true 则 rollback,否则 commit 
    // 默认情况下判断 (ex instanceof RuntimeException || ex instanceof Error)
    // 需要注意的是如果抛出的异常和 @Transactional 注解指定的 rollbackFor 不匹配,仍然会走到 super.rollbackOn(ex) 使用上面的默认规则兜底回滚
    // 回滚操作 txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus())
    // 提交操作 txInfo.getTransactionManager().commit(txInfo.getTransactionStatus())
    // 提交操作中如果 TransactionStatus.isRollbackOnly() 为 true 则同样会回滚事务
    completeTransactionAfterThrowing(txInfo, ex);
    throw ex;
}
finally {
    // 恢复当前线程 transactionInfoHolder 绑定的 txInfo 为 txInfo.oldTransactionInfo
    cleanupTransactionInfo(txInfo);
}
// 在成功完成调用后执行,如果创建了事务, 则会调用 commit 方法
commitTransactionAfterReturning(txInfo);
return retVal;

# Mybatis-Plus 事务失效

本地通过单元测试模拟事务回滚,事务方法被正常代理增强,但是在方法内部的 insert DAO 操作之后,数据库就立即可见了插入的数据,数据库的隔离级别是 REPEATABLE-READ, 看起来就像是事务被立即提交了,后面的代码抛出了 RuntimeException 也确实走到了切面环绕通知 catch 里的 rollback 操作,但是回滚无效。

继续 DEBUG Mybatis-Plus 的执行过程,DAO 代理执行过程如下

MybatisMapperProxy -> PlainMethodInvoker -> MybatisMapperMethod

SqlSessionTemplate -> DefaultSqlSession -> SimpleExecutor

最终发现 SimpleExecutor#prepareStatement 方法中的 getConnection 有点问题

mybatis get connection autocommit true

这里得到的 connection.autocommit=true, 类型是 com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl

查看 DataSourceUtils.getConnection(this.dataSource) 的流程如下

先 TransactionSynchronizationManager.getResource(dataSource) 获取绑定在当前线程的 ConnectionHolder
如果 ConnectionHolder 并且其中的 connection 不为空,则直接使用已绑定的 connection
否则 Connection con = fetchConnection(dataSource) 从数据源获取 connection
如果当前线程的事务同步处于活动状态,将 connection 绑定到当前线程

Is aware of a corresponding Connection bound to the current thread, for example when using DataSourceTransactionManager. Will bind a Connection to the thread if transaction synchronization is active (e.g. if in a JTA transaction).
如果使用的是 DataSourceTransactionManager 这里应该能直接拿到绑定到线程的 ConnectionHolder 如果在一个 JTA 事务中,则会将连接绑定到当前线程

如果使用 DataSourceTransactionManager, 在 tm.getTransaction(txAttr) 创建事务的时候,方法 doBegin 中初始化了 ConnectionHolder, 并且设置了 con.setAutoCommit(false). 这样后续我们在 Mybatis Executor 中拿到的应该是一个已经绑定在当前线程的 autocommit = false 的连接,事务应该可以正常回滚。

看到这里应该有点思路了,项目中由于定义了多个数据源,所以声明了 JtaTransactionManager 为默认事务管理器,但是我们使用的数据源还是 DruidDataSource 导致这里直接获取了一个原始连接,事务自动提交了,后续回滚已经没用了。

总结,事务管理器和使用的数据源必须保持一致,否则事务仍然可能失效。

这里我们的解决方案可以选择下面两种:

  • 数据源改成 AtomikosDataSource, 事务管理器仍然使用 JtaTransactionManager
    // 参考 com.baomidou.dynamic.datasource.creator.DefaultDataSourceCreator#createDataSource
    // 配置文件中需要指定 driver-class-name 和 type
    spring.datasource.dynamic.datasource.xxx.driver-class-name=com.mysql.jdbc.jdbc2.optional.MysqlXADataSource
    spring.datasource.dynamic.datasource.xxx.type=com.atomikos.jdbc.AtomikosDataSourceBean
    
  • 数据源不变,去掉默认的 JtaTransactionManager 声明,@Transactional 中使用单数据源的 DataSourceTransactionManager