Spring-事物NOT_SUPPORTED策略下多数据源切换的问题

​ 从案例中分析了NOT_SUPPORTED默认情况下为什么不能进行多数据源切换以及优雅的解决办法,以及给出了自己的思考

起因

今天写代码时遇到一个多数据源的切换问题,框架为Spring和Mybatus-Plus。在完全没事物的情况下pgsql和mysql切换没问题,但在@Transactional(propagation = Propagation.NOT_SUPPORTED)下却切换不了。翻了以前的Spring-Transactional文章和代码,遂记录一下原因和解决思路,以及整体的思考

​ 在有事物的情况下多数据源切换不了这是很正常的,毕竟Connection绑定到ThreadLocal了。但我开始简单的认为只要使用Propagation.NOT_SUPPORTED传播策略,毕竟这都不支持事物了,应该就能进行数据源切换。实际就是代码报错了,还是切换不了

伪代码

ProductService#recommend被调用在一个事物里

public class ProductService {

    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void recommend(int i){

        // pgsqlMapper这个mapper有注解 @DS("postgresql"),走pqsql库
        pgsqlMapper.queryById(i);
        
        // 走mysql库
        msyqlMapper.queryById(i);
    }
    
}

分析原因

​ ProductService#recommend使用了@Transactional(propagation = Propagation.NOT_SUPPORTED)注解,它构造出来的DefaultTransactionStatus中有如下两个字段需要重点关注。

public class DefaultTransactionStatus extends AbstractTransactionStatus {
    // 是否开启了真实的物理事物
    private final boolean newTransaction;
    // 是否由当前事务初始化并管理事务同步器(如注册连接绑定、hook 回调)
    private final boolean newSynchronization;
}

其中newTransaction=false, newSynchronization=true(受AbstractPlatformTransactionManager.transactionSynchronization字段控制)。

表示尽管当前未开启物理事务,但事务管理器仍会初始化事务同步器(TransactionSynchronizationManager.initSynchronization()),并允许资源绑定(如 ConnectionHolder)与注册事务hook(TransactionSynchronization)。

换句话说,“没有物理事务,但仍可进行事务性资源管理和 hook 回调”

​ 所以当触发pgsqlMapper.queryById(i)方法时,会绑定如下资源。(这一切必须要newSynchronization=true

  • 获取SqlSession时(SqlSessionUtils#getSqlSession)将SqlSessionFactory -> SqlSessionHolder 给缓存到TransactionSynchronizationManager#resources中
  • 获取Connection时(DataSourceUtils#doGetConnection)将DataSource -> ConnectionHolder 也缓存到TransactionSynchronizationManager#resources中

所以,当走到第二个方法msyqlMapper.queryById(i)时,会拿到上一步中缓存的SqlSessionHolder,内部操作的是同一个Connection(具体为DefaultSqlSession.executor.transaction.connection),即pgsql的Connection,所以报错了

解决办法

将这个字段AbstractPlatformTransactionManager.transactionSynchronization由默认的SYNCHRONIZATION_ALWAYS改为SYNCHRONIZATION_ON_ACTUAL_TRANSACTION。直接获取这个AbstractPlatformTransactionManager bean再进行改动有些不优雅,我翻了一遍发现有这个PlatformTransactionManagerCustomizer接口,可以对PlatformTransactionManager的子类进行定制化修改。所以可以添加如下的bean到容器中即可

@Component
public class TransactionSynchronizationCustomizer implements PlatformTransactionManagerCustomizer<AbstractPlatformTransactionManager>{

    @Override
    public void customize(AbstractPlatformTransactionManager transactionManager) {
        // 解决Propagation.NOT_SUPPORTED下多数据源不能切换动态切换的问题
        transactionManager.setTransactionSynchronization(AbstractPlatformTransactionManager.SYNCHRONIZATION_ON_ACTUAL_TRANSACTION);
    }
}

思考(重点)

​ 在 Spring 的事务模型中,Propagation.NOT_SUPPORTED 虽然会挂起当前事务,但默认配置下(SYNCHRONIZATION_ALWAYS)仍会注册事务同步器。
这导致即使没有物理事务,依然会出现线程绑定连接的行为(如通过 DataSourceUtils.getConnection() 获得的 Connection 会复用)。

​ 所以如果希望这个方法在语义上和“非事务环境”(即不使用@Transactional等情况)一致,避免隐式连接绑定,应将 transactionSynchronization 设置为 SYNCHRONIZATION_ON_ACTUAL_TRANSACTION

​ 看样子应该是Spring 为了支持某些非事务性方法(Propagation.NOT_SUPPORTED)也能参与事务资源控制才这样设计的