Quartz 集群环境下任务重复执行以及任务丢失

本文探讨了Quartz集群环境下遇到的任务重复执行和丢失问题。问题包括同一时刻任务在不同节点重复执行、定时任务意外丢失,以及暂停后重启任务导致被暂停期间的任务重跑。分析了misfire原理,解释了CronTriggerImpl和SimpleTriggerImpl的misfire策略,并提供了问题解决的参考链接和建议选择适合业务场景的misfire处理策略。

Quartz 集群环境下任务重复执行以及任务丢失

问题现象1:同一个时刻任务在两个集群节点分别执行了一次;
问题现象2:定时任务会出现意外的丢失;
问题现象3:暂停任务一段时间后,重新启用任务,发现被暂停期间的任务在启动后都被重新执行了一遍;
注:quartz版本为2.3.0,且使用JDBC模式存储Job:org.quartz.jobStore.class: org.quartz.impl.jdbcjobstore.JobStoreTX

Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,它可以与J2EE与J2SE应用程序相结合也可以单独使用。Quartz可以用来创建简单或为运行十个,百个,甚至是好几万个Jobs这样复杂的程序。

misfire 原理分析

任何系统都无法保证100% 在规定的时间内执行,在执行过程肯定会遇到种种问题(比如系统繁忙,无可用线程)导致任务延迟;在quartz 中 把这些延迟的任务称为misfire 任务,在quartz 中的任务延迟执行有个阀值(默认60s)当延迟超过这个阀值,当前任务被划入misfire 中;将不能正常的执行;所以quartz 为了保证不丢去任务,有相应策略来处理这些 misfire任务;业务系统中可以根据自己业务的场景来决定这些misfire 的任务丢弃还是在执行;

下面将分析源码中对misfire 的处理
在quratz 中 专门一个线程 MisfireHandler extends Thread 用来发现这些misfire的任务;

  @Override
        public void run() {
          在线程启动后将不断轮训
            while (!shutdown) {
                long sTime = System.currentTimeMillis();
                //这里是关键的地方:获取misfire 的任务,并根据不同的misfire的策略去处理相应的任务
                RecoverMisfiredJobsResult recoverMisfiredJobsResult = manage();
                if (recoverMisfiredJobsResult.getProcessedMisfiredTriggerCount() > 0) {
                //通知主线程 QuartzSchedulerThread 有新得到任务要加入
                    signalSchedulingChangeImmediately(recoverMisfiredJobsResult.getEarliestNewTime());
                }

                if (!shutdown) {
                //短暂的睡眠,主要作用是让其他线程有机会去执行
                    long timeToSleep = 50l;  // At least a short pause to help balance threads
                    if (!recoverMisfiredJobsResult.hasMoreMisfiredTriggers()) {
                        timeToSleep = getMisfireThreshold() - (System.currentTimeMillis() - sTime);
                        if (timeToSleep <= 0) {
                            timeToSleep = 50l;
                        }
                        if(numFails > 0) {
                            timeToSleep = Math.max(getDbRetryInterval(), timeToSleep);
                        }
                    }
                   =
                    try {
                        Thread.sleep(timeToSleep);
                    } catch (Exception ignore) {
                    }
                }//while !shutdown
            }
        }
    }
}
 接下来主要看 manage 方法中 doRecoverMisfires 的实现
 protected RecoverMisfiredJobsResult doRecoverMisfires() throws JobPersistenceException {
        boolean transOwner = false;
        Connection conn = getNonManagedTXConnection();
        try {
            RecoverMisfiredJobsResult result = RecoverMisfiredJobsResult.NO_OP;
           //这里就是去trigger 表查询所有misfire 的任务:主要是条件是(next_fire_time < now - 60s)
            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 {
                transOwner = getLockHandler().obtainLock(conn, LOCK_TRIGGER_ACCESS);
                //这个方法是根据配置的misfire 策略去处理这些misfire 任务
                result = recoverMisfiredJobs(conn, false);
            }
            
            commitConnection(conn);
            return result;
        } catch (JobPersistenceException e) {
            省略。。。。。
           }
    }
接下来主要看recoverMisfiredJobs 方法中 doUpdateOfMisfiredTrigger
 private void doUpdateOfMisfiredTrigger(Connection conn, OperableTrigger trig, boolean forceState, String newStateIfNotComplete, boolean recovering) throws JobPersistenceException {
        Calendar cal = null;
        if (trig.getCalendarName() != null) {
            cal = retrieveCalendar(conn, trig.getCalendarName());
        }
        schedSignaler.notifyTriggerListenersMisfired(trig);
        //根据不同的Trigger 实现类去处理不同的misfire 任务
        //这个方法并没有实际的去执行这些misfire 任务,他只是会根据misfire 策略,去修改下一次执行时间;(只要下一次时间修改了,主线程在轮训的时候就会重新发现这些misfire的任务)
        trig.updateAfterMisfire(cal);
        省略。。。。。
    }
不同的Trigger 实现类的misfire策略

下面来源于: https://dzone.com/articles/quartz-scheduler-misfire 的翻译

  • CronTriggerImpl
    以下例子基于下面的场景说明:
    早上 9 点到下午4点:
    执行一次:每个小时执行一次
    在执行过程中 9 点 ,10 点的任务都misfire了。到10:15 线程检测到misfire的任务
misfire instruction说明
withMisfireHandlingInstructionIgnoreMisfires所有错过的都会立即执行
withMisfireHandlingInstructionFireAndProceed立即执行第一个miffire 并丢弃其他misfire 的任务 无论错过了多少执行,都只执行一次立即执行
withMisfireHandlingInstructionDoNothing所有misfire 的任务都将被丢去不执行
  • SimpleTriggerImpl、
    以下例子基于下面的场景说明:
    早上 9 点到下午4点:
    1、 执行一次:每个小时执行一次
    2、重复执行:每个小时 执行8次
    在执行过程中 9 点 ,10 点的任务都misfire了。到10:15 线程检测到misfire的任务
misfire Instruction只执行一次的任务重复执行的任务
withMisfireHandlingInstructionFireNow立即执行misfire 的任务(但是只会执行一次misfire 的任务,比如多次发生misfire 将只会执行一次)同左
withMisfireHandlingInstructionIgnoreMisfires忽略misfire 策略,发现有misfire 任务立即执行同左
withMisfireHandlingInstructionNextWithExistingCount不触发misfire 任务不会立即执行misfire,misfire 将延续到下一个正常时间触发;比如 正常 9点到下午4点,因为9,10 执行misfire 了,这些misfire 将不断往后延:最终执行的时间范围变为 11 点到下午 6点
withMisfireHandlingInstructionNextWithRemainingCount不触发misfire 任务丢弃所有的misfire,执行总次数会减少
withMisfireHandlingInstructionNowWithExistingCount立即执行misfire 的任务立即执行第一个misfire,之后按正常的规则执行(总的执行次数不变)
withMisfireHandlingInstructionNowWithRemainingCount立即执行misfire 的任务立即执行第一个misfire ,剩余被丢弃(素所以总的执行次数会变少)。例如:在10:15,调度程序运行第一个misfire(9点错过执行的misfire),丢弃其他的misfire(上午10点错过执行的 misfire),并等待1小时来执行另外6个触发器:11:15、12:15、……下午4:15
问题解决:

关于第一个问题 模拟线上的环境 能100% 的复现出现重复的问题;可以参考 https://segmentfault.com/a/1190000015492260 博主分析已经比较全面了; 这里就不做细致的描述了;

第二个问题和第三个问题原因是一样的:因为系统繁忙没资源导致会导致misfire,同样 暂停一段时间后再恢复 也会导致misfie 的现象(这主要是quartz 本身的机制导致):之所以会导致任务丢失时因为线上配置的 是withMisfireHandlingInstructionDoNothing;

最后,这个misfire 策略需要根据不同的业务场景去选择不同的策略;

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值