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)也能参与事务资源控制才这样设计的