✨Quartz✨Misfire机制详解✨

1,601 阅读7分钟

傻笑的小埋

大家好,我是半夏之沫 😁😁 一名金融科技领域的JAVA系统研发😊😊
我希望将自己工作和学习中的经验以最朴实最严谨的方式分享给大家,共同进步👉💓👈
👉👉👉👉👉👉👉👉💓写作不易,期待大家的关注和点赞💓👈👈👈👈👈👈👈👈
👉👉👉👉👉👉👉👉💓关注微信公众号【技术探界】 💓👈👈👈👈👈👈👈👈


前言

在使用Quartz时,有时候我们的Trigger会因为一些原因导致无法在预期的时间点被触发,从而导致实际触发时间点相较于预期时间点有延迟,当延迟的时间超过默认的1分钟时,就会造成TriggerMisfire

本文将针对QuartzMisfire机制,进行机制说明与源码分析,希望帮助大家一文搞懂QuartzMisfire机制。

正文

一. Misfire机制说明

当发生如下情况让Trigger错过正常触发时间时,就会触发Misfire机制。

  1. 应用所有实例全部重启或宕机。此时没有实例来执行定时任务,可能导致Trigger错过触发时间;
  2. 不允许并发执行的Trigger上一次执行耗时超过了触发间隔。不能并发执行的Trigger如果上一次耗时超过了触发间隔,那么下一次触发时就会错过正常触发时间;
  3. 线程池没有可用线程。如果线程池长时间打满,会导致Trigger无法正常被触发,此时可能会导致Trigger错过正常触发时间。

Trigger触发Misfire机制时,根据Trigger的不同,有如下的策略进行选择。

1. SimpleTriggerMisfire机制

对应的官方注释在SimpleTrigger接口中

SimpleTriggerMisfire机制说明如下。

  1. MISFIRE_INSTRUCTION_FIRE_NOW(1)

如果TriggerREPEAT_COUNT == 0,则表现为在当前时刻立即触发一次。

如果TriggerREPEAT_COUNT > 0,则机制同MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT

  1. MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT(2)

在当前时刻立即触发一次,并以当前时刻作为起始时间,按照触发间隔依次往后触发,总触发次数不变,也就是TriggerFINAL_FIRE_TIME会往后移。

  1. MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT(3)

在当前时刻立即触发一次,并以当前时刻作为起始时间,按照触发间隔依次往后触发,总触发次数减少,减少的次数等于在Misfire期间错过的触发次数,也就是TriggerFINAL_FIRE_TIME保持(大致)不变。

  1. MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT(4)

在下一个触发时间点正常触发,总触发次数减少,减少的次数等于在Misfire期间错过的触发次数,也就是TriggerFINAL_FIRE_TIME保持(大致)不变。

  1. MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT(5)

在下一个触发时间点正常触发,并按照触发间隔依次往后触发,总触发次数不变,也就是TriggerFINAL_FIRE_TIME会往后移。

2. CronTriggerMisfire机制

对应的官方注释在CronTrigger接口中

CronTriggerMisfire机制说明如下。

  1. MISFIRE_INSTRUCTION_FIRE_NOW(1)

立即触发一次,后续按照正常的Cron计划来触发。

  1. MISFIRE_INSTRUCTION_DO_NOTHING(2)

什么都不做,后续按照正常的Cron计划来触发。

3. 公共的Misfire机制

对应的官方注释在Trigger接口中

公共的Misfire机制说明如下。

  1. MISFIRE_INSTRUCTION_SMART_POLICY(0)

QuartzMisfire的默认触发机制。

对于SimpleTrigger来说,会根据TriggerREPEAT_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触发机制。

  1. MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY(-1)

立即将所有错过的触发给补上。

SimpleTrigger为例,假如我们有一个Trigger的触发间隔是30sREPEAT_INTERVAL=30000),然后Trigger错过的时间达到了3分钟,此时在MisfireHandlingInstructionIgnoreMisfires机制下,会立刻一次性将错过的6次触发给补上,然后根据触发间隔来依次完成剩余的触发。

二. Misfire源码分析

在启动调度器的时候,会最终调用到MisfireHandlerinitialize() 方法,在这个方法中,就会将Misfire的后台线程运行起来,这个Misfire的后台线程,会不断的调用到JobStoreSupportdoRecoverMisfires() 方法来完成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是需要加锁的,但是如果很少出现MisfireTrigger,那么加锁开销太大,所以Quartz在处理Misfire前会先判断一下是否有需要MisfireTrigger,如果没有就不加锁了。

真正的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,需要同时满足如下条件。

  1. Trigger下一次触发时间(NEXT_FIRE_TIME)小于当前时间减去misfireThresholdmisfireThreshold默认是1分钟,也就是Trigger延迟触发的时间在1分钟内都可以容忍,超过1分钟才会被判定为Misfire
  2. qrtz_triggers表中Trigger状态是WAITING

判定为MisfireTrigger,后续就会根据相应的机制执行相应的Misfire逻辑。

总结

一个Trigger发生Misfire,其实就是这个Trigger因为一些原因错过了正常的触发时间点。Trigger要判定为Misfire,要满足如下条件。

  1. Trigger的状态是WAITING
  2. Trigger的下一次触发时间(NEXT_FIRE_TIME)小于当前时间减去misfireThreshold默认60s)。

什么情况会发生Misfire,通常有如下情况。

  1. 应用所有实例全部重启或宕机。此时没有实例来执行定时任务,可能导致Trigger错过触发时间;
  2. 不允许并发执行的Trigger上一次执行耗时超过了触发间隔。不能并发执行的Trigger如果上一次耗时超过了触发间隔,那么下一次触发时就会错过正常触发时间;
  3. 线程池没有可用线程。如果线程池长时间打满,会导致Trigger无法正常被触发,此时可能会导致Trigger错过正常触发时间。

如果发生了MisfireQuartz针对不同类型的Trigger提供了相应的弥补机制,具体机制说明可参见第一节的第123小节。


大家好,我是半夏之沫 😁😁 一名金融科技领域的JAVA系统研发😊😊
我希望将自己工作和学习中的经验以最朴实最严谨的方式分享给大家,共同进步👉💓👈
👉👉👉👉👉👉👉👉💓写作不易,期待大家的关注和点赞💓👈👈👈👈👈👈👈👈
👉👉👉👉👉👉👉👉💓关注微信公众号【技术探界】 💓👈👈👈👈👈👈👈👈

吃东西的小埋