Quartz管中窥豹之错过触发处理策略

在应用运行中,任务错过触发的事情也是时有发生,比如,系统宕机重启,在关闭至重启的时间,任务可能Misfired;线程池线程满了,没有空闲线程执行任务,导致无法触发,等待超时造成Misfired;不允许并发任务(@DisallowConcurrentExecution)在下次触发时间到达时,上次执行还未结束,只能等待,可能造成Misfired;触发器Trigger被暂停suspend时间段里,有些任务可能被Misfired等等。当然Quartz提供了一定的容忍度。配置文件可配置容忍错过触发阈值:org.quartz.jobStore.misfireThreshold=60000,默认为1分钟。意味着本因触发的任务未能及时执行可以延迟1分钟执行。超过1分钟后,即认为任务“Misfired”。这时就需要有错过触发的调度策略来决定如何这些“Misfired”任务。

错过触发处理策略

不同的触发器类型有自己的“Misfired”错过触发处理策略。可以查看各个触发器实现策略。

org.quartz.impl.triggers.AbstractTrigger.updateAfterMisfire(Calendar)

每个类型触发器要实现上述抽象类方法,进行“Misfired”任务处理。错过触发处理策略由MisfireHandler错过触发处理线程处理错过触发任务。总结如下:

序号 策略 描述 适用触发类型
1 MISFIRE_INSTRUCTION
_IGNORE_MISFIRE_POLICY
-1 这种策略会在资源适合的时候尽快的重新触发所有“Misfired”错过触发任务。不会影响现有的调度时间。例如,SimpleTrigger每15秒执行一次,而中间有5分钟时间它都“Misfired”了,一共错失了20个,5分钟后,假设资源充足了,并且任务允许并发,它会被一次性触发。 所有触发器
2 MISFIRE_INSTRUCTION
_SMART_POLICY
0 触发器默认策略。如果当前触发器是CronTrigger则走序号【3】策略。如果当前触发器是SimpleTrigger,当重复次数为0时,走序号【5】策略;当重复次数为-1,即无限次数时,走序号【8】策略;否则走序号【6】策略。如果当前触发器是
CalendarIntervalTrigger,则走序号【3】策略。如果当前触发器是
DailyTimeIntervalTrigger,则走序号【3】策略。
所有触发器
3 MISFIRE_INSTRUCTION
_FIRE_ONCE_NOW
1 立即触发“Misfired”错过触发任务。 CronTrigger

CalendarIntervalTrigger

DailyTimeIntervalTrigger

 

4 MISFIRE_INSTRUCTION
_DO_NOTHING
2 不立即触发执行,等待下次触发时间到达时刻才触发执行,重新开始调度任务。 CronTrigger

CalendarIntervalTrigger

DailyTimeIntervalTrigger

 

5 MISFIRE_INSTRUCTION
_FIRE_NOW
1 立即触发执行。此指令通常只适用于“一次性”(非重复)触发器。如果用于重复次数大于0的触发器,则走需要【7】策略 SimpleTrigger
6 MISFIRE_INSTRUCTION _RESCHEDULE_NOW_WITH
_EXISTING_REPEAT_COUNT
2 以当前时间为开始触发时间,立即触发执行。重复时间间隔不变,重复次数更新为原先重复次数-已触发次数。然后已触发次数重置为0。如果当前时间是在结束时间之后,触发器将不会再次触发。该策略无法记住最初设置的“开始时间”和“重复次数” SimpleTrigger
7 MISFIRE_INSTRUCTION
_RESCHEDULE_NOW_WITH
_REMAINING_REPEAT_COUNT
3 与序号【6】策略一致,区别在于重复次数的计算,重复次数更新为原先重复次数-已触发次数-下次触发时间到当前时间区间触发的次数。 SimpleTrigger
8 MISFIRE_INSTRUCTION
_RESCHEDULE_NEXT_WITH
_REMAINING_COUNT
4 等待下次触发时间到达时刻才触发执行,重新开始调度任务。会更新已触发次数为已触发次数+下个触发时间到当前时间的下个触发时间区间的次数。 SimpleTrigger
9 MISFIRE_INSTRUCTION
_RESCHEDULE_NEXT_WITH
_EXISTING_COUNT
5 等待下次触发时间到达时刻才触发执行,重新开始调度任务。

 

SimpleTrigger

错过触发处理策略测试

线程不足,相同时间同时触发多任务

测试结果:

当线程池满了,相同时间触发的任务只能等待有可用线程,或者非并发的任务上次触发执行太久,下次触发时间超过配置里设置的容忍错过触发阈值时,将被置为错过触发状态。由错过触发处理线程采用错过触发处理策略处理,或立即执行,或等待下一次触发等等,错过触发处理线程只会更新下次触发时间,并通知触发器监听器,最终还是由调度线程处理;如果没有超过阈值,则意味着已有可用线程可用,正常运行。因为线程池是按触发器的触发时间,然后才是触发器优先级来排任务队列的。一种错过触发任务一直等待的情形,只能是线程不足,而且线程都执行了长时间、甚至是死循环不会任务的这种情形。而线程饥饿是JVM进程调度的范畴,而Quartz调度处理线程中调度任务使用的线程池是SimpleThreadPool,默认的线程优先级是5,每个线程都是有机会参与CPU竞争的。而且Java优先级较高的线程不一定每一次都优先执行,有一定随机性。

测试步骤:

1、配置文件中设置任务线程池大小为1。改造FirstJob让它休眠60s。

FirstJob

2、还是定点相同时间触发多个任务。FistJob优先级最高优先执行。

trigger_on_the_same_time

3、SimpleJob和HelloJob处于等待状态。等待FirstJob线程结束时,另外两线程才相继触发。org.quartz.jobStore.misfireThreshold: 60000 设置容忍错过触发阈值为60s。如:SimpleJob本因55分触发的,如果56分还未触发,就会错过触发。由于线程池满了,SimpleJob只能等待FirstJob执行结束(即休眠60s)。60s后,FirstJob刚刚执行完毕,此时刚好在容忍错过触发阈值,错过触发。由MisfireHandler错过触发线程根据错过触发策略处理。如下图,处理了两个错过触发的任务。(这时候的策略是立即处理)

Misfired_handler

4、将容忍错过触发阈值加大为61s。即:org.quartz.jobStore.misfireThreshold: 61000

FirstJob线程结束后。其他两个任务此时未超过容忍错过触发阈值。按触发优先级争用线程池,正常处理。

normal_trigger

CronTrigger处理策略

使用Cron表达式,创建的触发器为CronTrigger,他采用的默认策略:

MISFIRE_INSTRUCTION_SMART_POLICY。对于CronTrigger,采用策略即立即触发。

MISFIRE_INSTRUCTION_SMART_POLICY

而CronTrigger暴露的错过触发策略:

CronTrigger_misfired_handler

即对应表格:

CronTrigger方法 对应策略
withMisfireHandlingInstructionIgnoreMisfires MISFIRE_INSTRUCTION_IGNORE_
MISFIRE_POLICY 序号【1】
withMisfireHandlingInstructionFireAndProceed MISFIRE_INSTRUCTION_FIRE_ONCE
_NOW 序号【3】
withMisfireHandlingInstructionDoNothing MISFIRE_INSTRUCTION_DO_NOTHING 序号【4】

观察其实现类org.quartz.impl.triggers.CronTriggerImpl,默认策略就是立即触发。

CronTriggerImpl.trigger_immediately

1、测试withMisfireHandlingInstructionDoNothing 即序号【4】策略。

创建触发器,带有withMisfireHandlingInstructionDoNothing

create_trigger_handlerwithnothing

2、设置容忍错过触发阈值为60s,让任务错过触发。改下触发时间,继续测试。

handlerwithnothing_result

3、根据打印日志可知,错过触发处理线程已经处理错过触发任务。但此时错过触发任务没有立即执行,要等待下一次执行。(下次执行时间是下一天对于的时间点,无法重现)

SimpleTrigger处理策略

1、测试SimpleTrigger的错过触发策略,定义StatefulDumbJob任务类,使用有状态和并发控制的Job,【有状态和并发Job测试】。将容忍错过触发阈值设为7000ms。线程数为2。

statefuldumbjob

2、定义两个任务,都是休眠10s,调度启动15s后,开始任务,后每3s触发,无限重复执行。Job1使用默认策略,因为定义的触发器SimpleTrigger及无限重复,由序号【1】策略,可知,最终走序号【8】策略

MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT;job2使用序号【6】策略MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT

MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT

3、查看测试结果,05分15秒开始两个任务触发。下个触发时间按周期3s,本应18s触发,但任务休眠10s后才能执行,刚好超过容忍错过触发的阈值,变成“Misfired”。由错过触发处理线程检测处理,05分27秒处理错过触发任务。发现job2立即执行,但job1在05分30秒才执行。第二次处理错过触发任务时,job2立即执行,但job1在05分42秒才执行。05分30秒、05分42秒、05分57秒都落在05分15秒开始,每3秒的周期点上,即会等待检测当前时间的下个触发时间触发错过触发任务。验证了序号【8】和【6】策略。

simpletrigger_result

代码参见

不同类型触发器定义了不同的公有静态常量来表示错过触发指令,用于错过触发任务处理,过触发指令可由以下接口找到

org.quartz.SimpleTrigger

org.quartz.CronTrigger

org.quartz.CalendarIntervalTrigger

org.quartz.DailyTimeIntervalTrigger

不同类型触发器实现AbstractTrigger抽象方法进行错过触发任务处理

org.quartz.impl.triggers.AbstractTrigger.updateAfterMisfire(Calendar)

总结

Quartz定义了容忍错过触发阈值和错过触发处理策略,不同类型的触发器可以有不同的触发策略,根据可能错过触发的场景,配置线程池大小也选的比较重要了,Quartz配置应该要能适应应用业务需要,动态调整,比如,可以上配置中心,等等。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

5 + 12 = ?