其他分享
首页 > 其他分享> > 困扰多年的Quartz重复调度的问题,终于找到原因

困扰多年的Quartz重复调度的问题,终于找到原因

作者:互联网

前言

内部系统基于Quartz做了定时调度模块。该模块不定期出现重复调度问题。此问题比较复杂,经常报警,且没有规律。
2019年就开始出现较多的问题,至今2021年,对此问题才得到比较清晰和完整的结论。

过程

最初的策略

增加未调度提醒,增加重复调度提醒。(毕竟这看起来是两个问题。)

后续策略

加强参数优化,监控负载情况,排除负载问题。

Misfire策略改动

修改Misfire Instruction。

Github Issue参考

设置 DisallowConcurrent
设置 acquireWithInLock

最新策略(无奈之举)

增加Quartz Listener中增加misfire的记录,严格记录发生时间。

分析

Quartz重复调度的原因(Cluster模式):

  1. Quartz Issue #107
  2. 错误的Misfire导致错误的重跑。

Quartz Issue #107

该问题原因比较复杂,参见Issue原文,做法就是添加注解或者相应配置:

acquireTriggersWithinLock=true

不正确使用Quartz API 导致的错误Misfire

可使用TriggerListener的API,监听,并结合所有调用Quartz API的调用打点分析:

org.quartz.TriggerListener#triggerMisfired

这里,经过观察,发现有一种场景比较常见:
即:经常对QuartzSchedule进行变更,且使用同一个triggerKey

根据Quartz的API源码:

org.quartz.Scheduler#scheduleJob(org.quartz.JobDetail, org.quartz.Trigger):

//org.quartz.impl.triggers.CronTriggerImpl.java

    @Override
    public Date computeFirstFireTime(org.quartz.Calendar calendar) {
        nextFireTime = getFireTimeAfter(new Date(getStartTime().getTime() - 1000l));

        while (nextFireTime != null && calendar != null
                && !calendar.isTimeIncluded(nextFireTime.getTime())) {
            nextFireTime = getFireTimeAfter(nextFireTime);
        }

        return nextFireTime;
    }

这里会根据getStartTime生成一个CronExpression的下一个执行时间。

如果startTime设置的是一个比较早的时间,则生成的nextFireTime会早于 now - threshold

经过层层调用

-> org.quartz.spi.JobStore#acquireNextTriggers
->    org.terracotta.quartz.DefaultClusteredJobStore#acquireNextTriggers
->       org.terracotta.quartz.DefaultClusteredJobStore#getNextTriggerWrappers
->           org.terracotta.quartz.DefaultClusteredJobStore#applyMisfire

执行applyMisfire的时候,如果满足
getNextFireTime + threshold < now
则导致 misFire触发,此时再根据Misfire Instruction判定是否重复触发,假如
Misfire Instruction=org.quartz.CronTrigger#MISFIRE_INSTRUCTION_FIRE_ONCE_NOW
则导致定时任务重复调度。

此处分析,对应观察到重复调度时间间隔,取决于调用Scheduler#scheduleJob和自然调度时间点的间隔。

结论

  1. 应当在复杂的并发条件下使用锁:
  2. Quartz API 构建Trigger应当使用正确的API
//Job&Trigger Key
JobKey jobKey = KeyUtil.jobKey(job);
TriggerKey triggerKey = KeyUtil.triggerKey(job, schedule);

//创建 触发器
TriggerBuilder triggerBuilder = TriggerBuilder.newTrigger()
        .withIdentity(triggerKey).forJob(jobKey)
        .startAt(schedule.getStartTime()); //注意此处的时间非常重要!!

后记

Quartz作为基础应用框架,虽然功能“看起来”比较简单,但是不要轻视他。

值得花一些时间定位问题。

本文覆盖的场景不代表全部,不能保证能解决所有重复调度的问题。需要系统化分析。

标签:nextFireTime,困扰,Quartz,调度,quartz,org,API
来源: https://www.cnblogs.com/slankka/p/quartzScheduleMisfireProblem.html