Spring-Quartz事务问题的排查总结

2,768 阅读3分钟

环境

  • Quartz集群模式,使用数据库持久化任务
  • Spring接管Quartz,即Quartz依赖于Spring运行

问题

在一套集群应用(4个实例节点)启动的时候,应用调用了QuartzScheduler#scheduleJob初始化定时任务,会偶然触发JobPersistenceException: could not store trigger : unique constraint violdated.即多个节点同时scheduleJob的时候了违反约束条件.

问题代码大概如下:

public class MyQuartzConfig {
    @Autowired
    private Scheduler scheduler;
    
    public void init() {
        scheduler.scheduleJob(job, trigger);
    }
}

我们都知道,Quartz集群模式是借用了数据库来实现竞争锁的,如果某个节点获取锁失败,会等待一段时间后再次尝试,直到超过重试次数抛出异常.上述出现违反约束条件肯定是因为锁没有生效,换句话说事务没有生效

原因

Quartz JobStore

Quartz提供了两个持久化任务的实现类,分别是JobStoreCMTJobStoreTX

  • JobStoreTX很简单,里面只有一个数据源,持久化的事务提交和回滚都由它来控制
  • JobStoreCMT有两个数据源,一个数据源TxDataSource,即包含事务的数据源,另一个NonTxDataSource,不包含事务的数据源

JobStoreCMT的两个数据源怎么理解呢?根据官方注释可以看到,TxDataSource事务受容器管理,可应用于JTA等XA场合. 而NonTxDataSource事务不受容器管理,是Quartz自行管理.

小结:CMT的NonTxDataSource即是JobStoreTX的数据源,Quartz获取connection后会设置autoCommit=false,由Quartz自行控制事务.

而JobStoreCMT的TxDataSource是额外添加的,为的是让用户能够控制这些事务,给用户更大的灵活性.

JobStore在不同的场景下都会用到这两种数据源,分别是executeInLockexecuteInNonManagedTXLock

  • executeInLock,与TxDataSource对应,基本上是提供给用户调用的API
  • executeInNonManagedTXLock,与NonTxDataSource对应,都是Quartz内部的API,与用户无关

Spring-Quartz

当Spring接管Quartz后,情况会发生一些变化. 如果Spring提供的SchedulerFactoryBean设置了数据源,就会用LocalDataSourceJobStore来替代Quartz的JobStore类.

LocalDataSourceJobStore其实是一个JobStoreCMT,它的TxDataSource受Spring事务管理,NonTxDataSource则不会.同时他会将从TxDataSource获取的connection.autoCommit=ture

真正原因

看到这里应该能猜到原因了,在我们的例子里,调用scheduleJob方法使用的数据源是TxDataSource,而持久化类LocalDataSourceJobStore默认autoCommit=ture,在这种情况下其实我们调用时是不存在事务的,自然就会出现文中的问题.

解决问题

没有事务的话给它加上事务就好了,由于我们用到Spring-Quartz,加上Spring的事务就能解决问题

public class MyQuartzConfig {
    @Autowired
    private Scheduler scheduler;
    @Transactional  // 加上事务注解即可解决问题
    public void init() {
        scheduler.scheduleJob(job, trigger);
    }
}

总结

Quartz提供JobStoreCMT是为了让用户调用Quartz的时候有事务控制(例如希望多个Quartz实例同时成功或同时失败).

我也不知道以后怎么避免这类问题,Spring提供的文档好像没有说明事务的问题,难道真的只能好好看注释了?