困扰多年的Quartz重复调度的问题,终于找到原因
作者:互联网
前言
内部系统基于Quartz做了定时调度模块。该模块不定期出现重复调度问题。此问题比较复杂,经常报警,且没有规律。
2019年就开始出现较多的问题,至今2021年,对此问题才得到比较清晰和完整的结论。
过程
最初的策略
增加未调度提醒,增加重复调度提醒。(毕竟这看起来是两个问题。)
后续策略
加强参数优化,监控负载情况,排除负载问题。
Misfire策略改动
修改Misfire Instruction。
Github Issue参考
设置 DisallowConcurrent
设置 acquireWithInLock
最新策略(无奈之举)
增加Quartz Listener中增加misfire的记录,严格记录发生时间。
分析
Quartz重复调度的原因(Cluster模式):
- Quartz Issue #107
- 错误的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
和自然调度时间点的间隔。
结论
- 应当在复杂的并发条件下使用锁:
- 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