✨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小节。


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

相关推荐
只因在人海中多看了你一眼1 小时前
分布式缓存 + 数据存储 + 消息队列知识体系
分布式·缓存
Theodore_10223 小时前
4 设计模式原则之接口隔离原则
java·开发语言·设计模式·java-ee·接口隔离原则·javaee
zhixingheyi_tian3 小时前
Spark 之 Aggregate
大数据·分布式·spark
冰帝海岸4 小时前
01-spring security认证笔记
java·笔记·spring
世间万物皆对象4 小时前
Spring Boot核心概念:日志管理
java·spring boot·单元测试
没书读了5 小时前
ssm框架-spring-spring声明式事务
java·数据库·spring
求积分不加C5 小时前
-bash: ./kafka-topics.sh: No such file or directory--解决方案
分布式·kafka
nathan05295 小时前
javaer快速上手kafka
分布式·kafka
小二·5 小时前
java基础面试题笔记(基础篇)
java·笔记·python
开心工作室_kaic5 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端