Quartz任务调度:MisFire策略和源码分析
Quartz是为大家熟知的任务调度框架,先看看官网的介绍:
-------------------------------------------------------------------------------------------------------------------------
What is the Quartz Job Scheduling Library?
Quartz is a richly featured, open source job scheduling library that can be integrated within virtually any Java application - from the smallest stand-alone application to the largest e-commerce system. Quartz can be used to create simple or complex schedules for executing tens, hundreds, or even tens-of-thousands of jobs; jobs whose tasks are defined as standard Java components that may execute virtually anything you may program them to do. The Quartz Scheduler includes many enterprise-class features, such as support for JTA transactions and clustering.
Quartz is freely usable, licensed under the Apache 2.0 license.
-------------------------------------------------------------------------------------------------------------------------
翻译:Quartz是一个功能丰富、开源的任务调度库,它可以集成到几乎任意Java应用中---小到最小的独立应用,大到最大的电子商务系统。Quartz 可以用来创建简单或者复杂的工作计划,同时执行数十、成百、甚至上万的任务。可被定义为标准Java组件的任务,几乎可以执行任意可以编程的任务。Quartz 任务调度包含许多企业级功能特性,比如支持JTA事务和集群。
Quartz可以免费试用,遵循 Apache 2.0 license 许可协议
-------------------------------------------------------------------------------------------------------------------------
公司项目也用的Quartz,最近遇到一些关于Quartz的问题,带着疑问,查阅了部分Quartz源码,与大家分享。
开始是为了研究Quartz的MisFire策略,当任务执行时间过长、服务停机、任务暂停等原因,导致其超过其下次执行的时间点时,就会涉及MisFire(失火,错误任务的触发)处理的策略问题。 Quartz的任务分为SimpleTrigger和CronTrigger,项目中一般使用CronTrigger居多,本文只涉及了CronTrigger的MisFire处理策略(SimpleTrigger的MisFire策略与CronTrigger不同,后续再说)。
MisFire策略常量的定义在类CronTrigger中,列举如下:
- MISFIRE_INSTRUCTION_FIRE_ONCE_NOW = 1
- MISFIRE_INSTRUCTION_DO_NOTHING = 2
- MISFIRE_INSTRUCTION_SMART_POLICY = 0
- MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY = -1
根据JavaDoc介绍和官网文档分析,其对应执行策略如下:
- MISFIRE_INSTRUCTION_FIRE_ONCE_NOW:立即执行一次,然后按照Cron定义时间点执行
- MISFIRE_INSTRUCTION_DO_NOTHING:什么都不做,等待Cron定义下次任务执行的时间点
- MISFIRE_INSTRUCTION_SMART_POLICY:智能的策略,针对不同的Trigger执行不同,CronTrigger时为MISFIRE_INSTRUCTION_FIRE_ONCE_NOW
- MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY:将所有错过的执行时间点全都补上,例如,任务15s执行一次,执行的任务错过了4分钟,则执行MisFire时,一次性执行4*(60/15)= 16次任务
但是,我写了例子,实际执行策略1和策略2与文档又不太相同,示例任务cron表达式为:0/10 * * * * ?,每10s执行一次。
测试步骤如下:
任务下次执行时间为15:05:10,misFire策略为MISFIRE_INSTRUCTION_FIRE_ONCE_NOW(1)
1.将任务暂停至15:05:35
2.重新启动任务,任务瞬间执行了3次
将misFire策略设置为MISFIRE_INSTRUCTION_DO_NOTHING与上述表现一致。这个实验结果与文档描述不太相符。
于是,翻阅Quartz源码,首先从定时任务本身入手,打断点,找到任务执行工作线程为:WorkerThread对象,工作线程池为:SimpleThreadPool
核心代码如下:
// WorkerThread.class
// 将任务送入工作线程
public void run(Runnable newRunnable) {
synchronized(lock) {
if(runnable != null) {
throw new IllegalStateException("Already running a Runnable!");
} runnable = newRunnable;
lock.notifyAll();
}
}
//循环执行,当有任务送入时执行任务
@Override
public void run() {
boolean ran = false; while (run.get()) {
try {
synchronized(lock) {
while (runnable == null && run.get()) {
lock.wait(500);
} if (runnable != null) {
ran = true;
runnable.run();
}
}
} catch (InterruptedException unblock) {
// do nothing (loop will terminate if shutdown() was called
try {
getLog().error("Worker thread was interrupt()'ed.", unblock);
} catch(Exception e) {
// ignore to help with a tomcat glitch
}
} catch (Throwable exceptionInRunnable) {
try {
getLog().error("Error while executing the Runnable: ",
exceptionInRunnable);
} catch(Exception e) {
// ignore to help with a tomcat glitch
}
} finally {
synchronized(lock) {
runnable = null;
}
// repair the thread in case the runnable mucked it up...
if(getPriority() != tp.getThreadPriority()) {
setPriority(tp.getThreadPriority());
} if (runOnce) {
run.set(false);
clearFromBusyWorkersList(this);
} else if(ran) {
ran = false;
makeAvailable(this);
} }
}
可以看到当有任务送入工作线程时,任务将被执行。由此,反向找到线程池代码,代码如下:
// SimpleThreadPool.class
public boolean runInThread(Runnable runnable) {
if (runnable == null) {
return false;
} synchronized (nextRunnableLock) { handoffPending = true; // Wait until a worker thread is available
while ((availWorkers.size() < 1) && !isShutdown) {
try {
nextRunnableLock.wait(500);
} catch (InterruptedException ignore) {
}
} if (!isShutdown) {
WorkerThread wt = (WorkerThread)availWorkers.removeFirst();
busyWorkers.add(wt);
wt.run(runnable);
} else {
// If the thread pool is going down, execute the Runnable
// within a new additional worker thread (no thread from the pool).
WorkerThread wt = new WorkerThread(this, threadGroup,
"WorkerThread-LastJob", prio, isMakeThreadsDaemons(), runnable);
busyWorkers.add(wt);
workers.add(wt);
wt.start();
}
nextRunnableLock.notifyAll();
handoffPending = false;
} return true;
}
可以看到线程池从可用的工作线程队列中取出一个工作线程,将任务送入工作线程(WorkerThread),然后任务会被执行。
由此,反向找到调用方法runInThread的地方,类QuartzSchedulerThread(约398行),QuartzSchedulerThread集成自Thread,又是一个无限循环执行的线程任务,找到类QuartzSchedulerThread.run()方法(由于代码量较大,此处不再全部粘贴),可以看到这个方法干的活大概是:循环找出需要执行的Job,然后送入线程池,再由线程池送入工作线程。
列举部分关键代码:
1.找出需要执行的Job的代码
try {
//此处去数据库查询将要执行的任务
triggers = qsRsrcs.getJobStore().acquireNextTriggers(
now + idleWaitTime, Math.min(availThreadCount, qsRsrcs.getMaxBatchSize()), qsRsrcs.getBatchTimeWindow());
acquiresFailed = 0;
if (log.isDebugEnabled())
log.debug("batch acquisition of " + (triggers == null ? 0 : triggers.size()) + " triggers");
} catch (JobPersistenceException jpe) {
if (acquiresFailed == 0) {
qs.notifySchedulerListenersError(
"An error occurred while scanning for the next triggers to fire.",
jpe);
}
if (acquiresFailed < Integer.MAX_VALUE)
acquiresFailed++;
continue;
} catch (RuntimeException e) {
if (acquiresFailed == 0) {
getLog().error("quartzSchedulerThreadLoop: RuntimeException "
+e.getMessage(), e);
}
if (acquiresFailed < Integer.MAX_VALUE)
acquiresFailed++;
continue;
}
关键点在注释处的代码,方法:acquireNextTriggers,继续debug跟进该方法,找到查询SQL,代码如下:
// StdJDBCDelegate.class
public List<TriggerKey> selectTriggerToAcquire(Connection conn, long noLaterThan, long noEarlierThan, int maxCount)
throws SQLException {
PreparedStatement ps = null;
ResultSet rs = null;
List<TriggerKey> nextTriggers = new LinkedList<TriggerKey>();
try {
ps = conn.prepareStatement(rtp(SELECT_NEXT_TRIGGER_TO_ACQUIRE)); // Set max rows to retrieve
if (maxCount < 1)
maxCount = 1; // we want at least one trigger back.
ps.setMaxRows(maxCount); // Try to give jdbc driver a hint to hopefully not pull over more than the few rows we actually need.
// Note: in some jdbc drivers, such as MySQL, you must set maxRows before fetchSize, or you get exception!
ps.setFetchSize(maxCount); ps.setString(1, STATE_WAITING);
ps.setBigDecimal(2, new BigDecimal(String.valueOf(noLaterThan)));
ps.setBigDecimal(3, new BigDecimal(String.valueOf(noEarlierThan)));
rs = ps.executeQuery(); while (rs.next() && nextTriggers.size() <= maxCount) {
nextTriggers.add(triggerKey(
rs.getString(COL_TRIGGER_NAME),
rs.getString(COL_TRIGGER_GROUP)));
} return nextTriggers;
} finally {
closeResultSet(rs);
closeStatement(ps);
}
}
根据debug时实时参数,处理过的SQL为:
SELECT
TRIGGER_NAME,
TRIGGER_GROUP,
NEXT_FIRE_TIME,
PRIORITY
FROM
qrtz_TRIGGERS
WHERE
SCHED_NAME = 'schedulerFactoryBean'
AND TRIGGER_STATE = 'WAITING'
AND NEXT_FIRE_TIME <= (now + idleWaitTime)
AND (
MISFIRE_INSTR = -1
OR (
MISFIRE_INSTR != -1
AND NEXT_FIRE_TIME >= (now - misfireThreshold)
)
)
ORDER BY NEXT_FIRE_TIME ASC, PRIORITY DESC
其中:now为系统当前时间,idleWaitTime为系统线程闲置时间,默认取值为30s,misfireThreshold为配置参数,意思为系统能容忍的misFire的最大阀值,默认为60s(当前系统配置也是60s,之前一直不知道这个值什么意思)。从SQL中看得很清楚了,这个SQL语句是要查询出:未来30s内将要执行的任务,且MISFIRE_INSTR为-1(MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY),或者MISFIRE_INSTR不为-1,但是,NEXT_FIRE_TIME错过的执行时间不能超过阀值60s。至此问题搞清楚了,影响misFire执行策略的另一个参数就是misfireThreshold,配置文件quartz.properties中,对应org.quartz.jobStore.misfireThreshold: 60000,单位毫秒。也就是说:如果【错过时间】不超过60s都不算是misFire,不执行misFire策略,依次执行错过的任务时间点;【错过时间】超过60s按misFire策略执行。
根据上述结论重新进行试验,将任务暂停时间超过60s,这次试验结果与文档描述一致。
另外,跟踪启动任务的代码,找到处理misFire的方法,代码位置:org.quartz.impl.triggers.CronTriggerImpl.updateAfterMisfire(Calendar)
@Override
public void updateAfterMisfire(org.quartz.Calendar cal) {
int instr = getMisfireInstruction(); if(instr == Trigger.MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY)
return; if (instr == MISFIRE_INSTRUCTION_SMART_POLICY) {
instr = MISFIRE_INSTRUCTION_FIRE_ONCE_NOW;
} if (instr == MISFIRE_INSTRUCTION_DO_NOTHING) {
Date newFireTime = getFireTimeAfter(new Date());
while (newFireTime != null && cal != null
&& !cal.isTimeIncluded(newFireTime.getTime())) {
newFireTime = getFireTimeAfter(newFireTime);
}
setNextFireTime(newFireTime);
} else if (instr == MISFIRE_INSTRUCTION_FIRE_ONCE_NOW) {
setNextFireTime(new Date());
}
}
可以清楚看到,misFire的执行逻辑。
在翻阅源码的同时,对之前比较疑惑的几个问题也做了研究,比如:Quartz的任务执行机制如何实现等等问题,都可以轻松通过翻阅源码找到答案,有兴趣的 童鞋 可以自己去翻阅下代码。
其实,针对这个问题,上网也可以查询问题的原因,但是,个人感觉由翻阅源码找到问题原因,对问题理解的更透彻,同时也能了解下Quartz的实现逻辑。鼓励大家遇到问题,去翻阅框架的源码,其实没有想象中的那么复杂。
(以上如有错误,还请指正,欢迎留言评论)
Quartz任务调度:MisFire策略和源码分析的更多相关文章
- Quartz学习--二 Hello Quartz! 和源码分析
Quartz学习--二 Hello Quartz! 和源码分析 三. Hello Quartz! 我会跟着 第一章 6.2 的图来 进行同步代码编写 简单入门示例: 创建一个新的java普通工程 ...
- Android Debuggerd 简要介绍和源码分析(转载)
转载: http://dylangao.com/2014/05/16/android-debuggerd-%E7%AE%80%E8%A6%81%E4%BB%8B%E7%BB%8D%E5%92%8C%E ...
- Java并发编程(七)ConcurrentLinkedQueue的实现原理和源码分析
相关文章 Java并发编程(一)线程定义.状态和属性 Java并发编程(二)同步 Java并发编程(三)volatile域 Java并发编程(四)Java内存模型 Java并发编程(五)Concurr ...
- Kubernetes Job Controller 原理和源码分析(一)
概述什么是 JobJob 入门示例Job 的 specPod Template并发问题其他属性 概述 Job 是主要的 Kubernetes 原生 Workload 资源之一,是在 Kubernete ...
- Kubernetes Job Controller 原理和源码分析(二)
概述程序入口Job controller 的创建Controller 对象NewController()podControlEventHandlerJob AddFunc DeleteFuncJob ...
- Kubernetes Job Controller 原理和源码分析(三)
概述Job controller 的启动processNextWorkItem()核心调谐逻辑入口 - syncJob()Pod 数量管理 - manageJob()小结 概述 源码版本:kubern ...
- jQuery静态方法globalEval使用和源码分析
Eval函数大家都很熟悉,但是globalEval方法却很少使用,大多数参考手册也没有相关api,下面就对其用法和源码相应介绍: jQuery.globalEval()函数用于全局性地执行一段Java ...
- Java线程池使用和源码分析
1.为什么使用线程池 在多线程编程中一项很重要的功能就是执行任务,而执行任务的方式有很多种,为什么一定需要使用线程池呢?下面我们使用Socket编程处理请求的功能,分别对每种执行任务的方式进行分析. ...
- RocketMQ中Broker的HA策略源码分析
Broker的HA策略分为两部分①同步元数据②同步消息数据 同步元数据 在Slave启动时,会启动一个定时任务用来从master同步元数据 if (role == BrokerRole.SLAVE) ...
随机推荐
- GitHub上最著名的Android播放器开源项目大全
GitHub上最著名的Android播放器开源项目大全 ...
- zblog模板怎么安装?zblog主题安装教程
有很多初次使用zblog的新手还会问我“zblog模板怎么安装?”,那么本文就专门给大家介绍下zblog主题安装教程. zblog现在分为zblogasp和zblogphp两个版本,两个版本的模板主题 ...
- Window和Linux文件共享
一.先设置window上的目录共享 1.1.右击文件要共享的文件夹,选择属性 1.2.设置要共享给的用户和设置用户操作权限 二.安装CIFS共享服务 sudo yum -y install cifs- ...
- 123457---com.threeObj.Baobaoshizi01--- 宝宝识字01
com.threeObj.Baobaoshizi01--- 宝宝识字01
- Django之中间件、缓存以及信号
Django之中间件 中间件执行流程 我们从浏览器发出一个请求 Request,得到一个响应后的内容 HttpResponse ,这个请求传递到 Django的过程如下: 也就是说,每一个请求都是先通 ...
- mysql数据format格式化错误
DROP TABLE IF EXISTS `api_billing`; CREATE TABLE `api_billing` ( `id` ) NOT NULL AUTO_INCREMENT, `se ...
- python面向对象之类属性,实例属性
python中的属性分为类属性和实例属性,之前已经说过一些,这里主要是对类属性与实例属性的增删改查 首先是对类属性的增删改查,下面这个是对类属性的修改,在书写类时,已经对类属性occupation进行 ...
- sonar:soanrqube接口api
背景: jenkins+sonar集成了代码扫描,但是发出的邮件不管项目质量是通过还是失败,每次邮件的标题都是jenkins的构建状态,所以需要获取sonar中该项目的扫描结果. 解决: 在sonar ...
- Pychar调用远程编译器的环境配置
版本:2019最新专业版
- Java基础系列-substring的原理
原创文章,转载请标注出处:https://www.cnblogs.com/V1haoge/p/10755235.html JDK 6和JDK 7中substring的原理及区别 substring(i ...