大家好,我是半夏之沫 😁😁 一名金融科技领域的JAVA系统研发😊😊
我希望将自己工作和学习中的经验以最朴实,最严谨的方式分享给大家,共同进步👉💓👈
👉👉👉👉👉👉👉👉💓写作不易,期待大家的关注和点赞💓👈👈👈👈👈👈👈👈
👉👉👉👉👉👉👉👉💓关注微信公众号【技术探界】 💓👈👈👈👈👈👈👈👈
前言
在使用Quartz时,有时候我们的Trigger会因为一些原因导致无法在预期的时间点被触发,从而导致实际触发时间点相较于预期时间点有延迟,当延迟的时间超过默认的1分钟时,就会造成Trigger的Misfire。
本文将针对Quartz的Misfire机制,进行机制说明与源码分析,希望帮助大家一文搞懂Quartz的Misfire机制。
正文
一. Misfire机制说明
当发生如下情况让Trigger错过正常触发时间时,就会触发Misfire机制。
- 应用所有实例全部重启或宕机。此时没有实例来执行定时任务,可能导致Trigger错过触发时间;
- 不允许并发执行的Trigger上一次执行耗时超过了触发间隔。不能并发执行的Trigger如果上一次耗时超过了触发间隔,那么下一次触发时就会错过正常触发时间;
- 线程池没有可用线程。如果线程池长时间打满,会导致Trigger无法正常被触发,此时可能会导致Trigger错过正常触发时间。
当Trigger触发Misfire机制时,根据Trigger的不同,有如下的策略进行选择。
1. SimpleTrigger的Misfire机制
(对应的官方注释在SimpleTrigger接口中)
SimpleTrigger的Misfire机制说明如下。
- MISFIRE_INSTRUCTION_FIRE_NOW(1)
如果Trigger的REPEAT_COUNT == 0,则表现为在当前时刻立即触发一次。
如果Trigger的REPEAT_COUNT > 0,则机制同MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT。
- MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT(2)
在当前时刻立即触发一次,并以当前时刻作为起始时间,按照触发间隔依次往后触发,总触发次数不变,也就是Trigger的FINAL_FIRE_TIME会往后移。
- MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT(3)
在当前时刻立即触发一次,并以当前时刻作为起始时间,按照触发间隔依次往后触发,总触发次数减少,减少的次数等于在Misfire期间错过的触发次数,也就是Trigger的FINAL_FIRE_TIME保持(大致)不变。
- MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT(4)
在下一个触发时间点正常触发,总触发次数减少,减少的次数等于在Misfire期间错过的触发次数,也就是Trigger的FINAL_FIRE_TIME保持(大致)不变。
- MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT(5)
在下一个触发时间点正常触发,并按照触发间隔依次往后触发,总触发次数不变,也就是Trigger的FINAL_FIRE_TIME会往后移。
2. CronTrigger的Misfire机制
(对应的官方注释在CronTrigger接口中)
CronTrigger的Misfire机制说明如下。
- MISFIRE_INSTRUCTION_FIRE_NOW(1)
立即触发一次,后续按照正常的Cron计划来触发。
- MISFIRE_INSTRUCTION_DO_NOTHING(2)
什么都不做,后续按照正常的Cron计划来触发。
3. 公共的Misfire机制
(对应的官方注释在Trigger接口中)
公共的Misfire机制说明如下。
- MISFIRE_INSTRUCTION_SMART_POLICY(0)
是Quartz的Misfire的默认触发机制。
对于SimpleTrigger来说,会根据Trigger的REPEAT_COUNT的值来决定使用哪种Misfire机制。
如果REPEAT_COUNT == 0,则使用MISFIRE_INSTRUCTION_FIRE_NOW机制;
如果REPEAT_COUNT0 > 0,则使用MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT;
如果REPEAT_COUNT0 == REPEAT_INDEFINITELY,也就是重复无限次,则使用MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT。
对于CronTrigger来说,MISFIRE_INSTRUCTION_SMART_POLICY会使用MISFIRE_INSTRUCTION_FIRE_NOW触发机制。
- MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY(-1)
立即将所有错过的触发给补上。
以SimpleTrigger为例,假如我们有一个Trigger的触发间隔是30s(REPEAT_INTERVAL=30000),然后Trigger错过的时间达到了3分钟,此时在MisfireHandlingInstructionIgnoreMisfires机制下,会立刻一次性将错过的6次触发给补上,然后根据触发间隔来依次完成剩余的触发。
二. Misfire源码分析
在启动调度器的时候,会最终调用到MisfireHandler的initialize() 方法,在这个方法中,就会将Misfire的后台线程运行起来,这个Misfire的后台线程,会不断的调用到JobStoreSupport的doRecoverMisfires() 方法来完成Misfire,下面从doRecoverMisfires() 方法开始分析。
protected RecoverMisfiredJobsResult doRecoverMisfires() throws JobPersistenceException {
boolean transOwner = false;
Connection conn = getNonManagedTXConnection();
try {
RecoverMisfiredJobsResult result = RecoverMisfiredJobsResult.NO_OP;
// 如果doubleCheckLockMisfireHandler配置为true
// 则在获取TRIGGER_ACCESS前先判断是否有Misfire的Trigger
// 以减少TRIGGER_ACCESS锁的获取
// 默认是true但如果总是存在Misfire的Trigger则要配成false
int misfireCount = (getDoubleCheckLockMisfireHandler()) ?
getDelegate().countMisfiredTriggersInState(
conn, STATE_WAITING, getMisfireTime()) :
Integer.MAX_VALUE;
if (misfireCount == 0) {
getLog().debug(
"Found 0 triggers that missed their scheduled fire-time.");
} else {
// 获取TRIGGER_ACCESS锁
transOwner = getLockHandler().obtainLock(conn, LOCK_TRIGGER_ACCESS);
// 实际的Misfire逻辑
result = recoverMisfiredJobs(conn, false);
}
// 提交事务
commitConnection(conn);
return result;
} catch (JobPersistenceException e) {
rollbackConnection(conn);
throw e;
} catch (SQLException e) {
rollbackConnection(conn);
throw new JobPersistenceException("Database error recovering from misfires.", e);
} catch (RuntimeException e) {
rollbackConnection(conn);
throw new JobPersistenceException("Unexpected runtime exception: "
+ e.getMessage(), e);
} finally {
try {
releaseLock(LOCK_TRIGGER_ACCESS, transOwner);
} finally {
cleanupConnection(conn);
}
}
}
处理Misfire是需要加锁的,但是如果很少出现Misfire的Trigger,那么加锁开销太大,所以Quartz在处理Misfire前会先判断一下是否有需要Misfire的Trigger,如果没有就不加锁了。
真正的Misfire逻辑在recoverMisfiredJobs() 方法中,下面再跟进一下。
protected RecoverMisfiredJobsResult recoverMisfiredJobs(
Connection conn, boolean recovering)
throws JobPersistenceException, SQLException {
// 默认一个事务中处理的Misfire的Trigger数为20
int maxMisfiresToHandleAtATime =
(recovering) ? -1 : getMaxMisfiresToHandleAtATime();
List<TriggerKey> misfiredTriggers = new LinkedList<TriggerKey>();
long earliestNewTime = Long.MAX_VALUE;
// 判断为Misfire的需要满足如下条件
// 1. Trigger下一次触发时间小于当前时间减去misfireThreshold
// 2. qrtz_triggers表中Trigger状态是WAITING
// 其中misfireThreshold默认是1分钟
// 也就是Trigger延迟触发的时间在1分钟内都可以容忍
// 如果超过1分钟则判定为Misfire
boolean hasMoreMisfiredTriggers =
getDelegate().hasMisfiredTriggersInState(
conn, STATE_WAITING, getMisfireTime(),
maxMisfiresToHandleAtATime, misfiredTriggers);
if (hasMoreMisfiredTriggers) {
getLog().info(
"Handling the first " + misfiredTriggers.size() +
" triggers that missed their scheduled fire-time. " +
"More misfired triggers remain to be processed.");
} else if (misfiredTriggers.size() > 0) {
getLog().info(
"Handling " + misfiredTriggers.size() +
" trigger(s) that missed their scheduled fire-time.");
} else {
getLog().debug(
"Found 0 triggers that missed their scheduled fire-time.");
return RecoverMisfiredJobsResult.NO_OP;
}
for (TriggerKey triggerKey: misfiredTriggers) {
OperableTrigger trig =
retrieveTrigger(conn, triggerKey);
if (trig == null) {
continue;
}
// 在这里获取Misfire的机制并执行相应的逻辑
doUpdateOfMisfiredTrigger(conn, trig, false, STATE_WAITING, recovering);
if(trig.getNextFireTime() != null && trig.getNextFireTime().getTime() < earliestNewTime)
earliestNewTime = trig.getNextFireTime().getTime();
}
return new RecoverMisfiredJobsResult(
hasMoreMisfiredTriggers, misfiredTriggers.size(), earliestNewTime);
}
这里需要知道一个Trigger如何被判定为Misfire,需要同时满足如下条件。
- Trigger下一次触发时间(NEXT_FIRE_TIME)小于当前时间减去misfireThreshold。misfireThreshold默认是1分钟,也就是Trigger延迟触发的时间在1分钟内都可以容忍,超过1分钟才会被判定为Misfire;
- qrtz_triggers表中Trigger状态是WAITING。
判定为Misfire的Trigger,后续就会根据相应的机制执行相应的Misfire逻辑。
总结
一个Trigger发生Misfire,其实就是这个Trigger因为一些原因错过了正常的触发时间点。Trigger要判定为Misfire,要满足如下条件。
- Trigger的状态是WAITING;
- Trigger的下一次触发时间(NEXT_FIRE_TIME)小于当前时间减去misfireThreshold(默认60s)。
什么情况会发生Misfire,通常有如下情况。
- 应用所有实例全部重启或宕机。此时没有实例来执行定时任务,可能导致Trigger错过触发时间;
- 不允许并发执行的Trigger上一次执行耗时超过了触发间隔。不能并发执行的Trigger如果上一次耗时超过了触发间隔,那么下一次触发时就会错过正常触发时间;
- 线程池没有可用线程。如果线程池长时间打满,会导致Trigger无法正常被触发,此时可能会导致Trigger错过正常触发时间。
如果发生了Misfire,Quartz针对不同类型的Trigger提供了相应的弥补机制,具体机制说明可参见第一节的第1,2和3小节。
大家好,我是半夏之沫 😁😁 一名金融科技领域的JAVA系统研发😊😊
我希望将自己工作和学习中的经验以最朴实,最严谨的方式分享给大家,共同进步👉💓👈
👉👉👉👉👉👉👉👉💓写作不易,期待大家的关注和点赞💓👈👈👈👈👈👈👈👈
👉👉👉👉👉👉👉👉💓关注微信公众号【技术探界】 💓👈👈👈👈👈👈👈👈