type
status
date
slug
summary
tags
category
icon
password
AI 摘要
项目上遇到一个Bug
服务中会偶发性的出现No value for key [HikariDataSource (HikariPool-1)] bound to thread [线程名称]这类报错。
经过详细的排查,结合源码分析,总算是把问题弄明白了,在这里记录一下。
问题背景
在项目上偶发会出现下面的类似报错
一般在出现该异常后,出现异常的线程后续事务执行异常,导致数据未回滚、数据提交未生效或者出现连接关闭(Connection is closed)等问题
问题分析
通过报错的堆栈信息可以了解到是在事务操作过程中出现的问题,从事务拦截器开启事务到事务提交,清理事务资源,最后在进行解绑资源时出现了异常。
事务处理流程梳理
整个事务处理的流程大概如下:
事务资源绑定
绑定资源是事务管理器中的DataSourceTransactionManager#doBegin方法中
obtainDataSource 方法则是获取事务管理器DataSourceTransactionManager中的DataSource对象
TransactionSynchronizationManager.bindResource方法源码
主要功能是将当前的DataSource与Connection绑定到当前线程中,这里的resources是一个ThreadLocal变量,内部则是一个Map将Datasource与Connection进行键值关联。
通过这种关联,以实现在同一个事务保证使用同一个连接访问数据库。
事务的解绑则是在DataSourceTransactionManager#doCleanupAfterCompletion方法中。
同样是通过obtainDataSource()方法获取当前事务管理器上的DataSource对象,将其作为Key传入TransactionSynchronizationManager.unbindResource中进行解绑。
TransactionSynchronizationManager.unbindResource()资源解绑方法
把传入的Key 作为当前线程变量resources中的Map的Key进行匹配,如果没有匹配到则会报错
No value for key [" + actualKey + "] bound to thread [" + Thread.currentThread().getName() + "]
多数据源配置
因为项目中使用到了多数据源配置,并且初始化事务管理器时,指定了DataSource为我们自定义的一个Datasource实现类:MultiRoutingDataSource
在MultiDataSourceProxy#init方法中,对配置的多数据源进行了处理,重新封装了一个MultiRoutingDataSource对象并赋值给了事务管理器中的Datasource
因此在绑定资源时,obtainDataSource获取到的是一个MultiRoutingDataSource对象实例。
由于MultiRoutingDataSource实现了determineCurrentLookupKey方法,因此在实际获取连接时会通过MultiDataSourceHelper.getDataSource()获取当前线程上设置的数据源标识,从而在MultiRoutingDataSource的TargetDataSources这个Map中通过标识获取到一个DataSource实例对象,实现数据源的动态切换。
手动事务
但在我们另一个业务逻辑中,使用到了手动事务,并且在注入事务管理器时Datasource重新进行了赋值。
通过
DataSource.class
名称获取到的实际上是HikariDataSource的实例对象,即MultiRoutingDataSource中配置的DefaultTargetDataSource。因为在执行完这个业务逻辑后,事务管理器中的数据源实际上变成了一个HikariDataSource实例对象,并不是MultiRoutingDataSource对象了。问题复现
在绑定资源时,是通过获取当前事务管理器上的DataSource与连接进行绑定的,因此解绑也需要使用同一个DataSource进行解绑。
如果我们在事务绑定资源前与绑定后,这个数据源发生了变化,那么我们在解绑资源时则会出错。
问题场景
当一个大事务在处理期间,当前服务又执行了业务操作2重新赋值事务管理器数据源的操作时,则大事务在结束释放资源时会出现资源绑定使用的数据源不一致,导致解绑失败报错的问题。
这里画了一张图帮助理解一下问题场景:
按上图的流程,我们只需要执行一个大事务的业务操作1,紧接着在事务没有结束之前去执行一个业务操作2,在业务操作2中包含了更改事务管理器数据源的逻辑,这样事务管理器上的数据源被修改,等大事务结束释放资源时则会报错。
同时执行前如果使用Arthas监控事务管理器的获取方法,可以看到事务管理器中的Datasource发生了变化。
在第三次监控到DataSourceTransactionManager的dataSource已经变成了一个HikariDataSource实例,名称就是HikariDataSource (HikariPool-1) 与报错的数据源名称吻合
修复方案
删除业务操作2中的重新赋值数据源的逻辑,直接使用初始化好的MultiRoutingDataSource即可。
因为MultiRoutingDataSource中的DefaultTargetDataSource就是默认的HikariDataSource数据源,如果业务操作2中不需要切换数据源则直接会默认使用DefaultTargetDataSource数据源。
Loading...