先简述一下问题及原因:系统采用了动态数据源并引入了两个数据源,QuartzConfig配置了两个调度器分别对应了两个数据源。事务管理器是单个,并且根据上下文切换要处理的数据源。QuartzConfig中两个调度器,数据源分别配置两个单一数据源,如果配置事务管理器,那么事务管理器中的dataSource与调度器是不一致的(前者为动态数据源类型,后者为单数据源DruidDataSource),所以导致了Quartz在进行操作时(删除Trigger、JobDetail等)无法继承并使用当前线程已经开启的事务。
通过修改调度器的数据源配置可以解决此问题,详情见这篇文章有详细的分析过程和解决办法:动态数据源/多数据源Quartz事务问题踩坑
修改调度器的数据源配置可以解决Quartz事务问题,但因为调度器数据源改成了动态,并且操作的数据源是由上下文决定而不是调度器,因此在调度器执行过程中,若数据源发生切换,可能会引发其他问题,所以本文意在探究如何在不更改Quartz配置(让调度器仍然绑定固定的数据源)情况下,解决此问题。
从数据库连接入手
首先查看Quartz配置文件中有一个配置Quartz具体持久化操作时用的类:org.springframework.scheduling.quartz.LocalDataSourceJobStore
,这是Quartz提供的,进入此类可以看到获取连接的方式是使用Spring提供的工具类,经过实测可以发现:若调度器数据源与事务管理器一致时,这样获取是能直接取到当前事务的连接,否则就会取一个新的连接。
所以想要修改这个问题,就需要复写这个类,并且修改此处
解决步骤
- 这里首先要创建一个ConnectionManager,设置一个ThreadLocal用来存储已经开启了事务的连接,供Quartz操作前获取
public class ConnectionManager {
private static final ThreadLocal<Connection> connectionHolder = new ThreadLocal<>();
public static Connection getConnection() {
return connectionHolder.get();
}
public static void setConnection(Connection connection) {
connectionHolder.set(connection);
}
public static void removeConnection() {
connectionHolder.remove();
}
}
- 还需要一个获取当前事务使用的数据库连接的方法,可以放在一个Service中,具体实现如下(系统情况不同可能获取方式不完全一致)
public static Connection getCurrentDynamicRoutingDataSourceConnection(DataSource currentDynamicRoutingDataSource) throws SQLException {
if (DynamicDataSourceContextHolder.getCurrentTransactionDataSourceCode() != null
&& !DynamicDataSourceContextHolder.getCurrentTransactionDataSourceCode().equals(DynamicDataSourceContextHolder.getDataSourceCode())) {
return currentDynamicRoutingDataSource.getConnection();
}
ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(currentDynamicRoutingDataSource);
if (conHolder != null && conHolder.getConnectionHandle() != null) {
DruidPooledConnection connection = (DruidPooledConnection) conHolder.getConnection();
ConnectionProxyImpl connectionProxyImpl = (ConnectionProxyImpl) connection.getConnection();
Assert.notNull(connectionProxyImpl, "connectionProxyImpl is null!");
DataSourceProxy dataSourceProxy = connectionProxyImpl.getDirectDataSource();
Assert.notNull(dataSourceProxy, "dataSourceProxy is null!");
String dataSourceName = dataSourceProxy.getName();
if (!dataSourceName.equals(DynamicDataSourceContextHolder.getDataSourceCode())) {
return currentDynamicRoutingDataSource.getConnection();
}
}
return DataSourceUtils.getConnection(currentDynamicRoutingDataSource);
}
- 创建后,我们在具体业务函数内(已经开启事务的函数,例如使用@Transactional或手动开启),通过上述方法获取连接并将其存入ThreadLocal中,并在finally中释放
@Autowired
DataSource dataSource;
public Object test() {
Connection connection = getCurrentDynamicRoutingDataSourceConnection(dataSource);
ConnectionManager.setConnection(connection);
try {
// 业务代码
// ...
} finally {
ConnectionManager.removeConnection();
}
}
- 需要新创建一个类以供Quartz持久化时调用,并且修改获取、关闭连接处,整体可以copy
org.springframework.scheduling.quartz.LocalDataSourceJobStore
,修改处如下
如果采用此种方式进行事务,那么关闭连接处应当不执行任何操作(如果这里关闭连接,后续Quartz操作将无法使用从ThreadLocal中获取的连接,并且会抛出异常),这里为了适配不采用事务的情况(ThreadLocal中不存在conn时正常关闭连接)
最后别忘了将新建的Quartz持久化类配置到Quartz的配置文件中
org.quartz.jobStore.class=com.xxx.xxxx.CustomJobStoreCMT
至此,就可以让Quartz操作正常参与到事务中了。业务类前后置操作看起来比较冗余,可以考虑新增自定义注解(以@Transactional为元注解),配合切面对有Quartz操作的函数进行统一处理,这里不展开说写法了。
总结
通过重写Quartz持久化类,并修改其获取连接方式,可以达到让Quartz操作继承已经开启的事务并正常提交、回滚。