Spring事务资源解绑异常问题

🐞 项目上遇到一个Bug 服务中会偶发性的出现No value for key [HikariDataSource (HikariPool-1)] bound to thread [线程名称]这类报错。 经过详细的排查,结合源码分析,总算是把问题弄明白了,在这里记录一下。
Spring事务资源解绑异常问题
🐞
项目上遇到一个Bug 服务中会偶发性的出现No value for key [HikariDataSource (HikariPool-1)] bound to thread [线程名称]这类报错。
经过详细的排查,结合源码分析,总算是把问题弄明白了,在这里记录一下。

问题背景

在项目上偶发会出现下面的类似报错

java

2024-07-24 10:26:22.423 [线程名称] ERROR org.xxxxx.core.exception.BaseExceptionHandler - No value for key [HikariDataSource (HikariPool-1)] bound to thread [线程名称] java.lang.IllegalStateException: No value for key [HikariDataSource (HikariPool-1)] bound to thread [线程名称]
Java
一般在出现该异常后,出现异常的线程后续事务执行异常,导致数据未回滚、数据提交未生效或者出现连接关闭(Connection is closed)等问题
 

问题分析

通过报错的堆栈信息可以了解到是在事务操作过程中出现的问题,从事务拦截器开启事务到事务提交,清理事务资源,最后在进行解绑资源时出现了异常。

plain

java.lang.IllegalStateException: No value for key [HikariDataSource (HikariPool-1)] bound to thread [线程名称] at org.springframework.transaction.support.TransactionSynchronizationManager.unbindResource(TransactionSynchronizationManager.java:213) at org.springframework.jdbc.datasource.DataSourceTransactionManager.doCleanupAfterCompletion(DataSourceTransactionManager.java:367) at org.springframework.transaction.support.AbstractPlatformTransactionManager.cleanupAfterCompletion(AbstractPlatformTransactionManager.java:1007) at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:793) at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:714) at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:532) at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:304) at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:98)
Plain text
 

事务处理流程梳理

整个事务处理的流程大概如下:
notion image

事务资源绑定

绑定资源是事务管理器中的DataSourceTransactionManager#doBegin方法中

java

protected void doBegin(Object transaction, TransactionDefinition definition) { DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; Connection con = null; try { if (!txObject.hasConnectionHolder() || txObject.getConnectionHolder().isSynchronizedWithTransaction()) { //通过当前事务管理器上的Datasource获取一个连接 Connection newCon = obtainDataSource().getConnection(); if (logger.isDebugEnabled()) { logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction"); } //将连接封装为一个ConnectionHolder txObject.setConnectionHolder(new ConnectionHolder(newCon), true); } //省略 // Bind the connection holder to the thread. //绑定当前事务管事器上的数据源与连接 if (txObject.isNewConnectionHolder()) { TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder()); } } // 省略 }
Java
obtainDataSource 方法则是获取事务管理器DataSourceTransactionManager中的DataSource对象

java

protected DataSource obtainDataSource() { DataSource dataSource = getDataSource(); Assert.state(dataSource != null, "No DataSource set"); return dataSource; } public DataSource getDataSource() { return this.dataSource; }
Java
TransactionSynchronizationManager.bindResource方法源码
主要功能是将当前的DataSource与Connection绑定到当前线程中,这里的resources是一个ThreadLocal变量,内部则是一个MapDatasource与Connection进行键值关联。
通过这种关联,以实现在同一个事务保证使用同一个连接访问数据库。

java

public static void bindResource(Object key, Object value) throws IllegalStateException { Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key); Assert.notNull(value, "Value must not be null"); Map<Object, Object> map =resources.get(); // set ThreadLocal Map if none found if (map == null) { map = new HashMap<>(); resources.set(map); } Object oldValue = map.put(actualKey, value); // Transparently suppress a ResourceHolder that was marked as void... if (oldValue instanceof ResourceHolder && ((ResourceHolder) oldValue).isVoid()) { oldValue = null; } if (oldValue != null) { throw new IllegalStateException("Already value [" + oldValue + "] for key [" + actualKey + "] bound to thread [" + Thread.currentThread().getName() + "]"); } if (logger.isTraceEnabled()) { logger.trace("Bound value [" + value + "] for key [" + actualKey + "] to thread [" + Thread.currentThread().getName() + "]"); } }
Java
事务的解绑则是在DataSourceTransactionManager#doCleanupAfterCompletion方法中。
同样是通过obtainDataSource()方法获取当前事务管理器上的DataSource对象,将其作为Key传入TransactionSynchronizationManager.unbindResource中进行解绑。

java

protected void doCleanupAfterCompletion(Object transaction) { DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; // Remove the connection holder from the thread, if exposed. if (txObject.isNewConnectionHolder()) { //解绑资源 TransactionSynchronizationManager.unbindResource(obtainDataSource()); } // Reset connection. Connection con = txObject.getConnectionHolder().getConnection(); //省略 }
Java
TransactionSynchronizationManager.unbindResource()资源解绑方法
把传入的Key 作为当前线程变量resources中的Map的Key进行匹配,如果没有匹配到则会报错
No value for key [" + actualKey + "] bound to thread [" + Thread.currentThread().getName() + "]

java

public static Object unbindResource(Object key) throws IllegalStateException { Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key); Object value =doUnbindResource(actualKey); if (value == null) { throw new IllegalStateException( "No value for key [" + actualKey + "] bound to thread [" + Thread.currentThread().getName() + "]"); } return value; } private static Object doUnbindResource(Object actualKey) { Map<Object, Object> map =resources.get(); if (map == null) { return null; } Object value = map.remove(actualKey); // Remove entire ThreadLocal if empty... if (map.isEmpty()) { resources.remove(); } // Transparently suppress a ResourceHolder that was marked as void... if (value instanceof ResourceHolder && ((ResourceHolder) value).isVoid()) { value = null; } if (value != null &&logger.isTraceEnabled()) { logger.trace("Removed value [" + value + "] for key [" + actualKey + "] from thread [" + Thread.currentThread().getName() + "]"); } return value; }
Java

多数据源配置

因为项目中使用到了多数据源配置,并且初始化事务管理器时,指定了DataSource为我们自定义的一个Datasource实现类:MultiRoutingDataSource

java

public class MultiRoutingDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return MultiDataSourceHelper.getDataSource(); } }
Java
在MultiDataSourceProxy#init方法中,对配置的多数据源进行了处理,重新封装了一个MultiRoutingDataSource对象并赋值给了事务管理器中的Datasource

java

@Component @Slf4j public class MultiDataSourceProxy { @Autowired @Qualifier("dataSource") private DataSource dataSource; @Autowired private SqlSessionFactory sqlSessionFactory; @Autowired private PlatformTransactionManager transactionManager; @Autowired private DefaultListableBeanFactory beanFactory; @PostConstruct private void init() { // 解析数据源,并转换为集合 Map<Object, Object> multiDataSourceMap = new HashMap<>(); Map<String, DataSource> dataSourceBeanMap = this.beanFactory.getBeansOfType(DataSource.class, false, true); dataSourceBeanMap.forEach((k, v) -> { if (!v.equals(this.dataSource)){ multiDataSourceMap.put(k.toUpperCase(), v); multiDataSourceMap.put(k.toUpperCase().replace("DATASOURCE", ""), v); } }); // 如果没有配置多数据源,直接退出 if (MapUtils.isEmpty(multiDataSourceMap)){ return; } // 设置主库 multiDataSourceMap.put("master", this.dataSource); // 创建多切换数据源 MultiRoutingDataSource multiRoutingDataSource = new MultiRoutingDataSource(); multiRoutingDataSource.setDefaultTargetDataSource(this.dataSource); multiRoutingDataSource.setTargetDataSources(multiDataSourceMap); multiRoutingDataSource.afterPropertiesSet(); this.sqlSessionFactory.getConfiguration().setEnvironment(new Environment(sqlSessionFactory.getConfiguration().getEnvironment().getId(), sqlSessionFactory.getConfiguration().getEnvironment().getTransactionFactory(), multiRoutingDataSource)); //这里改变了事务管理器的默认数据源 ((DataSourceTransactionManager)this.transactionManager).setDataSource(multiRoutingDataSource); } }
Java
因此在绑定资源时,obtainDataSource获取到的是一个MultiRoutingDataSource对象实例。
由于MultiRoutingDataSource实现了determineCurrentLookupKey方法,因此在实际获取连接时会通过MultiDataSourceHelper.getDataSource()获取当前线程上设置的数据源标识,从而在MultiRoutingDataSource的TargetDataSources这个Map中通过标识获取到一个DataSource实例对象,实现数据源的动态切换。

手动事务

但在我们另一个业务逻辑中,使用到了手动事务,并且在注入事务管理器时Datasource重新进行了赋值。

java

this.transactionManager = (DataSourceTransactionManager)ApplicationContextHelper.getContext().getBean(DataSourceTransactionManager.class); this.transactionManager.setDataSource((DataSource)ApplicationContextHelper.getContext().getBean("dataSource"));
Java
通过DataSource.class名称获取到的实际上是HikariDataSource的实例对象,即MultiRoutingDataSource中配置的DefaultTargetDataSource。因为在执行完这个业务逻辑后,事务管理器中的数据源实际上变成了一个HikariDataSource实例对象,并不是MultiRoutingDataSource对象了。

问题复现

在绑定资源时,是通过获取当前事务管理器上的DataSource与连接进行绑定的,因此解绑也需要使用同一个DataSource进行解绑。
如果我们在事务绑定资源前与绑定后,这个数据源发生了变化,那么我们在解绑资源时则会出错。

问题场景

当一个大事务在处理期间,当前服务又执行了业务操作2重新赋值事务管理器数据源的操作时,则大事务在结束释放资源时会出现资源绑定使用的数据源不一致,导致解绑失败报错的问题。
这里画了一张图帮助理解一下问题场景:
notion image
按上图的流程,我们只需要执行一个大事务的业务操作1,紧接着在事务没有结束之前去执行一个业务操作2,在业务操作2中包含了更改事务管理器数据源的逻辑,这样事务管理器上的数据源被修改,等大事务结束释放资源时则会报错。

java

public static Object unbindResource(Object key) throws IllegalStateException { Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key); Object value = doUnbindResource(actualKey); if (value == null) { throw new IllegalStateException( "No value for key [" + actualKey + "] bound to thread [" + Thread.currentThread().getName() + "]"); } return value; }
Java
 
同时执行前如果使用Arthas监控事务管理器的获取方法,可以看到事务管理器中的Datasource发生了变化。
在第三次监控到DataSourceTransactionManager的dataSource已经变成了一个HikariDataSource实例,名称就是HikariDataSource (HikariPool-1) 与报错的数据源名称吻合

shell

[arthas@26778]$ watch org.springframework.transaction.interceptor.TransactionAspectSupport determineTransactionManager '{returnObj}' -n 5 -x 2 Press Q or Ctrl+C to abort. Affect(class count: 2 , method count: 1) cost in 1462 ms, listenerId: 1 method=org.springframework.transaction.interceptor.TransactionAspectSupport.determineTransactionManager location=AtExit ts=2024-08-09 11:07:13; [cost=4.482292ms] result=@ArrayList[ @DataSourceTransactionManager[ dataSource=@MultiRoutingDataSource[org.xxx.xxx.xxx.config.MultiRoutingDataSource@39db0b8e], enforceReadOnly=@Boolean[false], SYNCHRONIZATION_ALWAYS=@Integer[0], SYNCHRONIZATION_ON_ACTUAL_TRANSACTION=@Integer[1], SYNCHRONIZATION_NEVER=@Integer[2], constants=@Constants[org.springframework.core.Constants@42837d6a], logger=@SLF4JLocationAwareLog[org.apache.commons.logging.impl.SLF4JLocationAwareLog@6e74cc9f], transactionSynchronization=@Integer[0], defaultTimeout=@Integer[-1], nestedTransactionAllowed=@Boolean[true], validateExistingTransaction=@Boolean[false], globalRollbackOnParticipationFailure=@Boolean[true], failEarlyOnGlobalRollbackOnly=@Boolean[false], rollbackOnCommitFailure=@Boolean[false], ], ] method=org.springframework.transaction.interceptor.TransactionAspectSupport.determineTransactionManager location=AtExit ts=2024-08-09 11:07:14; [cost=0.111096ms] result=@ArrayList[ @DataSourceTransactionManager[ dataSource=@MultiRoutingDataSource[org..xxx.xxx.xxx.config.MultiRoutingDataSource@39db0b8e], enforceReadOnly=@Boolean[false], SYNCHRONIZATION_ALWAYS=@Integer[0], SYNCHRONIZATION_ON_ACTUAL_TRANSACTION=@Integer[1], SYNCHRONIZATION_NEVER=@Integer[2], constants=@Constants[org.springframework.core.Constants@42837d6a], logger=@SLF4JLocationAwareLog[org.apache.commons.logging.impl.SLF4JLocationAwareLog@6e74cc9f], transactionSynchronization=@Integer[0], defaultTimeout=@Integer[-1], nestedTransactionAllowed=@Boolean[true], validateExistingTransaction=@Boolean[false], globalRollbackOnParticipationFailure=@Boolean[true], failEarlyOnGlobalRollbackOnly=@Boolean[false], rollbackOnCommitFailure=@Boolean[false], ], ] method=org.springframework.transaction.interceptor.TransactionAspectSupport.determineTransactionManager location=AtExit ts=2024-08-09 11:07:15; [cost=0.124596ms] result=@ArrayList[ @DataSourceTransactionManager[ dataSource=@HikariDataSource$$EnhancerBySpringCGLIB$$75e8f8fd[HikariDataSource (HikariPool-1)], enforceReadOnly=@Boolean[false], SYNCHRONIZATION_ALWAYS=@Integer[0], SYNCHRONIZATION_ON_ACTUAL_TRANSACTION=@Integer[1], SYNCHRONIZATION_NEVER=@Integer[2], constants=@Constants[org.springframework.core.Constants@42837d6a], logger=@SLF4JLocationAwareLog[org.apache.commons.logging.impl.SLF4JLocationAwareLog@6e74cc9f], transactionSynchronization=@Integer[0], defaultTimeout=@Integer[-1], nestedTransactionAllowed=@Boolean[true], validateExistingTransaction=@Boolean[false], globalRollbackOnParticipationFailure=@Boolean[true], failEarlyOnGlobalRollbackOnly=@Boolean[false], rollbackOnCommitFailure=@Boolean[false], ], ]
Shell
 

修复方案

删除业务操作2中的重新赋值数据源的逻辑,直接使用初始化好的MultiRoutingDataSource即可。
因为MultiRoutingDataSource中的DefaultTargetDataSource就是默认的HikariDataSource数据源,如果业务操作2中不需要切换数据源则直接会默认使用DefaultTargetDataSource数据源。
上一篇
Mysql迁移StartRocks记录
下一篇
Mybatis Log Parser插件
Loading...
2025-3-5
最新发布
Spring事务资源解绑异常问题
2025-3-5
智能IDE与插件集成DeepSeek指南:开发者的高效编程新选择
2025-3-5
Account Note:一款解决网站账号管理烦恼的浏览器扩展
2025-3-5
ChatGPT与豆包的图像生成
2024-11-12
Windows10家庭版安装Docker记录
2024-11-12
Mybatis Log Parser插件
2024-11-11