✨Quartz✨Misfire机制详解✨


前言

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

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

正文

一. Misfire机制说明

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

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

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

1. SimpleTrigger Misfire机制

对应的官方注释在 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. CronTrigger Misfire机制

对应的官方注释在 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() 方法开始分析。

java 复制代码
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() 方法中,下面再跟进一下。

java 复制代码
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小节。


总结不易,如果本文对你有帮助,烦请点赞,收藏加关注,谢谢帅气漂亮的你。

相关推荐
Mr Aokey31 分钟前
手写Java线程池与定时器:彻底掌握多线程任务调度
java·开发语言
西瓜本瓜@4 小时前
在Android中如何使用Protobuf上传协议
android·java·开发语言·git·学习·android-studio
言之。4 小时前
别学了,打会王者吧
java·python·mysql·容器·spark·php·html5
机智的人猿泰山4 小时前
java kafka
java·开发语言·kafka
Algorithm15764 小时前
谈谈接口和抽象类有什么区别?
java·开发语言
yu4106214 小时前
Rust 语言使用场景分析
开发语言·后端·rust
龙仔7255 小时前
离线安装rabbitmq全流程
分布式·rabbitmq·ruby
细心的莽夫5 小时前
SpringCloud 微服务复习笔记
java·spring boot·笔记·后端·spring·spring cloud·微服务
jack_xu6 小时前
高频面试题:如何保证数据库和es数据一致性
后端·mysql·elasticsearch