背景:在项目中使用Quartz很久,此次在做迭代开发时突然发现Quartz的操作(使用调度器创建、删除trigger或jobDetail等操作)没有正常读取并执行线程内的事务,而在此接口内,其他Mybatis操作都可以正常执行事务。
由于排查过程过于曲折漫长,这里省下大部分弯路,直接记录有效的分析过程。
双数据源引起的问题
首先项目是双数据源,为了确保不是Quartz常规配置的问题,另搭建了一套SpringBoot,仅引入MyBatis和Quartz,并且版本与出问题的项目保持一致,尝试后发现事务无问题,又将demo参照出问题的项目改为了双数据源(动态数据源),果然复现了事务问题,可以把问题范围缩小至多数据源的问题或与之相关的其他问题。
补充一点,如果需要Quartz支持事务,在配置文件中org.quartz.jobStore.class需要配置为org.springframework.scheduling.quartz.LocalDataSourceJobStore,这里配置的是将Quartz相关数据(Trigger、JobDetail)持久化时选用的操作类,LocalDataSourceJobStore继承了JobStoreCMT,后者是Quartz提供的支持事务的持久化类。
Quartz配置缺少事务管理器
本系统中Quartz的数据源是在配置类QuartzConfig中配置的,因为分为系统和业务库,所以将Quartz也配置了两个对应的调度器Scheduler,并在对应调度器中设置对应的数据源。
查询一些文章有提到,需要在配置调度器这里配置事务管理器,这样Quartz才能继承Spring中的这个事务管理器从而让Quartz操作的事务生效,于是加了红框中这两行。
注:注入的transactionManager为系统中自定义的事务管理器
这样修改后测试仍不能正常执行事务,继续排查
动态数据源问题
经过层层排查可以确定,在给Quartz调度器配置了Spring中的事务管理器后,事务仍不能生效的原因可能是:调度器的dataSource与事务管理器内部的dataSource不一致。
于是在项目启动时debug对比这两处的dataSource实际类型。
A.从动态数据源上下文获取的DataSource:
由于类型为自定义连接池数据源类,是一个继承了DruidDataSource的类,实际是一个单数据源的DataSource对象而非动态数据源的DataSource对象。(由于各个系统动态数据源处理的方式不尽相同,这里不展开说为什么从上下文中获取的为单数据源DataSource对象)
B.事务管理器中的DataSource:
可以看出两处并非同类型的DataSource,前者为单数据源,后者为动态数据源(DynamicRoutingDataSource为自定义动态数据源类,继承于AbstractRoutingDataSource)。
解决方案
将QuartzConfig配置中的DataSource改为从Spring容器中获取的动态数据源类,与事务管理器中的dataSource完全一致,这样函数在执行Quartz操作时,如果该函数启动了事务(@Transactional或手动开启)则Quartz也会正常在该事务内进行Quartz相关的数据库操作。
至此,问题就可以得到解决,一个开启事务的函数中,包含多个Quartz操作以及其他数据库操作,所有数据库操作均会在同一事务中进行。
疑问点
如此修改后虽然可以解决事务的问题,但此时调度器实际是绑定了动态数据源,也就是说调度器操作的数据源是可变的了,依据你系统中数据源上下文来决定,需要充分考虑这种情况是否会对系统中其他地方有影响。如果调度器的数据源不允许修改(可能出于其他方面考虑),可以参考这个方案:Quartz强制继承当前线程事务
总结
问题出现的根本原因:A.调度器未配置正确的事务管理器,需要把Spring的事务管理器(包括自定义事务管理器)配置给调度器;B.调度器配置的数据源类型与事务管理器不一致,即便二者指向同一数据库,但由于类型不一致无法识别,所以不会遵循事务管理器的规则正常执行事务,需要修正调度器配置的数据源。