quartz 深入

Quartz 是 OpenSymphony 开源组织在任务调度领域的一个开源项目,完全基于 Java 实现。作为一个优秀的开源调度框架,Quartz 具有功能强大,应用灵活,易于集成的特点。
和JDK Timer API的比较

       目前已有的调度框架除了Flux Scheduler, Enterprise Batching Queuing等一些商业版本之外,还有JDK自带的Timer和TimerTask,对于商业版本不作任何评估,目前只是对能免费使用的两个常用的调度 框架做一些比较:
JDK Timer/TimerTask和Quartz的比较
JDK Timer/TimerTask Quartz
每个任务一个线程 线程池
只是基于内存的调度 内存&数据库
不能支持很复杂的调度 CronTrigger可以定义复杂的调

Quartz 基本概念及原理
Quartz Scheduler 开源框架
Quartz 是 OpenSymphony 开源组织在任务调度领域的一个开源项目,完全基于 Java 实现。该项目于 2009 年被 Terracotta 收购,目前是 Terracotta 旗下的一个项目。读者可以到 http://www.quartz-scheduler.org/站点下载 Quartz 的发布版本及其源代码。笔者在产品开发中使用的是版本 1.8.4,因此本文内容基于该版本。本文不仅介绍如何应用 Quartz 进行开发,也对其内部实现原理作一定讲解。
作为一个优秀的开源调度框架,Quartz 具有以下特点:
强大的调度功能,例如支持丰富多样的调度方法,可以满足各种常规及特殊需求;
灵活的应用方式,例如支持任务和调度的多种组合方式,支持调度数据的多种存储方式;
分布式和集群能力,Terracotta 收购后在原来功能基础上作了进一步提升。本文暂不讨论该部分内容
另外,作为 Spring 默认的调度框架,Quartz 很容易与 Spring 集成实现灵活可配置的调度功能。
下面是本文中用到的一些专用词汇,在此声明:
scheduler:
任务调度器
trigger:
触发器,用于定义任务调度时间规则
job:
任务,即被调度的任务
misfire:
错过的,指本来应该被执行但实际没有被执行的任务调度
Quartz 任务调度的基本实现原理
核心元素
Quartz 任务调度的核心元素是 scheduler, trigger 和 job,其中 trigger 和 job 是任务调度的元数据, scheduler 是实际执行调度的控制器。
在 Quartz 中,trigger 是用于定义调度时间的元素,即按照什么时间规则去执行任务。Quartz 中主要提供了四种类型的 trigger:SimpleTrigger,CronTirgger,DateIntervalTrigger,和 NthIncludedDayTrigger。这四种 trigger 可以满足企业应用中的绝大部分需求。我们将在企业应用一节中进一步讨论四种 trigger 的功能。
在 Quartz 中,job 用于表示被调度的任务。主要有两种类型的 job:无状态的(stateless)和有状态的(stateful)。对于同一个 trigger 来说,有状态的 job 不能被并行执行,只有上一次触发的任务被执行完之后,才能触发下一次执行。Job 主要有两种属性:volatility 和 durability,其中 volatility 表示任务是否被持久化到数据库存储,而 durability 表示在没有 trigger 关联的时候任务是否被保留。两者都是在值为 true 的时候任务被持久化或保留。一个 job 可以被多个 trigger 关联,但是一个 trigger 只能关联一个 job。
在 Quartz 中, scheduler 由 scheduler 工厂创建:DirectSchedulerFactory 或者 StdSchedulerFactory。 第二种工厂 StdSchedulerFactory 使用较多,因为 DirectSchedulerFactory 使用起来不够方便,需要作许多详细的手工编码设置。 Scheduler 主要有三种:RemoteMBeanScheduler, RemoteScheduler 和 StdScheduler。本文以最常用的 StdScheduler 为例讲解。这也是笔者在项目中所使用的 scheduler 类。

线程视图
在 Quartz 中,有两类线程,Scheduler 调度线程和任务执行线程,其中任务执行线程通常使用一个线程池维护一组线程。

Scheduler 调度线程主要有两个: 执行常规调度的线程,和执行 misfired trigger 的线程。常规调度线程轮询存储的所有 trigger,如果有需要触发的 trigger,即到达了下一次触发的时间,则从任务执行线程池获取一个空闲线程,执行与该 trigger 关联的任务。Misfire 线程是扫描所有的 trigger,查看是否有 misfired trigger,如果有的话根据 misfire 的策略分别处理。
关于 misfired trigger,我们在企业应用一节中将进一步描述。
数据存储
Quartz 中的 trigger 和 job 需要存储下来才能被使用。Quartz 中有两种存储方式:RAMJobStore, JobStoreSupport,其中 RAMJobStore 是将 trigger 和 job 存储在内存中,而 JobStoreSupport 是基于 jdbc 将 trigger 和 job 存储到数据库中。RAMJobStore 的存取速度非常快,但是由于其在系统被停止后所有的数据都会丢失,所以在通常应用中,都是使用 JobStoreSupport。
在 Quartz 中,JobStoreSupport 使用一个驱动代理来操作 trigger 和 job 的数据存储:StdJDBCDelegate。StdJDBCDelegate 实现了大部分基于标准 JDBC 的功能接口,但是对于各种数据库来说,需要根据其具体实现的特点做某些特殊处理,因此各种数据库需要扩展 StdJDBCDelegate 以实现这些特殊处理。Quartz 已经自带了一些数据库的扩展实现,可以直接使用
作为嵌入式数据库的代表,Derby 近来非常流行。如果使用 Derby 数据库,可以使用上图中的 CloudscapeDelegate 作为 trigger 和 job 数据存储的代理类。
回页首
基本开发流程及简单实例
搭建开发环境
利用 Quartz 进行开发相当简单,只需要将下载开发包中的 quartz-all-1.8.4.jar 加入到 classpath 即可。根据笔者的经验,对于任务调度功能比较复杂的企业级应用来说,最好在开发阶段将 Quartz 的源代码导入到开发环境中来。一方面可以通过阅读源码了解 Quartz 的实现机理,另一方面可以通过扩展或修改 Quartz 的一些类来实现某些 Quartz 尚不提供的功能。

上图中左边是源码导入后的截图,其中 org.quartz.* 即为 quartz 的源码。导入源码后可能会有一些编译错误,通常出现在 org.quartz.ee.* 和 org.quartz.jobs.ee.* 包中。下载开发包中有一个 lib 目录,读者可以将该目录下的 jar 文件加入到编译环境。如果还有编译错误,读者可以参考上图中右侧的 jar 列表,到网上去搜索下载。
项目中 com.ibm.zxn.sample.quartz 是我们自己的类包,下面的实例中我们会用到它。
一个简单实例
Quartz 开发包中有一个 examples 目录,其中有 15 个基本实例。建议读者阅读并实践这些例子。本文这里只列举一个小的实例,介绍基本的开发方法。
准备数据库和 Quartz 用的数据表
本文使用 IBM DB2 数据库:将 jdbc 驱动程序 db2jcc.jar 加入到项目中;
在数据库中创建一个新库 QUARTZDB;
执行 /quartz-1.8.4/docs/dbTables/tables_db2_v8.sql,创建数据表
准备配置文件,加入到项目中

通过实现 job 接口定义我们自己的任务类

然后,实现任务调度的主程序,如下所示:
本实例中,我们利用 DateIntervalTrigger 实现一个每两分钟执行一次的任务调度。
完成后项目结构如下所示:
运行程序,查看数据库表和运行结果
数据库中,QRTZ_TRIGGERS 表中添加了一条 trigger 记录,如下所示:

QRTZ_JOB_DETAILS 表中添加了一条 job 记录,如下所示:

从运行结果来看,任务每两分钟被执行一次:

回页首
企业级开发中的常见应用
在应用 Quartz 进行企业级的开发时,有一些问题会经常遇到。本节笔者根据自己在项目开发中的经验,介绍企业开发中常见的一些问题以及通常的解决办法。
应用一:如何使用不同类型的 Trigger
前面我们提到 Quartz 中四种类型的 Trigger:SimpleTrigger,CronTirgger,DateIntervalTrigger, 和 NthIncludedDayTrigger。
SimpleTrigger 一般用于实现每隔一定时间执行任务,以及重复多少次,如每 2 小时执行一次,重复执行 5 次。SimpleTrigger 内部实现机制是通过计算间隔时间来计算下次的执行时间,这就导致其不适合调度定时的任务。例如我们想每天的 1:00AM 执行任务,如果使用 SimpleTrigger 的话间隔时间就是一天。注意这里就会有一个问题,即当有 misfired 的任务并且恢复执行时,该执行时间是随机的(取决于何时执行 misfired 的任务,例如某天的 3:00PM)。这会导致之后每天的执行时间都会变成 3:00PM,而不是我们原来期望的 1:00AM。
CronTirgger 类似于 LINUX 上的任务调度命令 crontab,即利用一个包含 7 个字段的表达式来表示时间调度方式。例如,"0 15 10 * * ? *" 表示每天的 10:15AM 执行任务。对于涉及到星期和月份的调度,CronTirgger 是最适合的,甚至某些情况下是唯一选择。例如,"0 10 14 ? 3 WED" 表示三月份的每个星期三的下午 14:10PM 执行任务。读者可以在具体用到该 trigger 时再详细了解每个字段的含义。
DateIntervalTrigger 是 Quartz 1.7 之后的版本加入的,其最适合调度类似每 N(1, 2, 3...)小时,每 N 天,每 N 周等的任务。虽然 SimpleTrigger 也能实现类似的任务,但是 DateIntervalTrigger 不会受到我们上面说到的 misfired 任务的影响。另外,DateIntervalTrigger 也不会受到 DST(Daylight Saving Time, 即中国的夏令时)调整的影响。笔者就曾经因为该原因将项目中的 SimpleTrigger 改为了 DateIntervalTrigger,因为如果使用 SimpleTrigger,本来设定的调度时间就会由于 DST 的调整而提前或延迟一个小时,而 DateIntervalTrigger 不会受此影响。
NthIncludedDayTrigger 的用途比较简单明确,即用于每隔一个周期的第几天调度任务,例如,每个月的第 3 天执行指定的任务。
除了上面提到的 4 种 Trigger,Quartz 中还定义了一个 Calendar 类(注意,是 org.quartz.Calendar)。这个 Calendar 与 Trigger 一起使用,但是它们的作用相反,它是用于排除任务不被执行的情况。例如,按照 Trigger 的规则在 10 月 1 号需要执行任务,但是 Calendar 指定了 10 月 1 号是节日(国庆),所以任务在这一天将不会被执行。通常来说,Calendar 用于排除节假日的任务调度,从而使任务只在工作日执行。
应用二:使用有状态(StatefulJob)还是无状态的任务(Job)
在 Quartz 中,Job 是一个接口,企业应用需要实现这个接口以定义自己的任务。基本来说,任务分为有状态和无状态两种。实现 Job 接口的任务缺省为无状态的。Quartz 中还有另外一个接口 StatefulJob。实现 StatefulJob 接口的任务为有状态的,上一节的简单实例中,我们定义的 SampleJob 就是实现了 StatefulJob 接口的有状态任务。下图列出了 Quartz 中 Job 接口的定义以及一些自带的实现类:

无状态任务一般指可以并发的任务,即任务之间是独立的,不会互相干扰。例如我们定义一个 trigger,每 2 分钟执行一次,但是某些情况下一个任务可能需要 3 分钟才能执行完,这样,在上一个任务还处在执行状态时,下一次触发时间已经到了。对于无状态任务,只要触发时间到了就会被执行,因为几个相同任务可以并发执行。但是对有状态任务来说,是不能并发执行的,同一时间只能有一个任务在执行。
在笔者项目中,某些任务需要对数据库中的数据进行增删改处理。这些任务不能并发执行,否则会造成数据混乱。因此我们使用 StatefulJob 接口。现在回到上面的例子,任务每 2 分钟执行一次,若某次任务执行了 5 分钟才完成,Quartz 会怎么处理呢?按照 trigger 的规则,第 2 分钟和第 4 分钟分别会有一次预定的触发执行,但是由于是有状态任务,因此实际不会被触发。在第 5 分钟第一次任务执行完毕时,Quartz 会把第 2 和第 4 分钟的两次触发作为 misfired job 进行处理。对于 misfired job,Quartz 会查看其 misfire 策略是如何设定的,如果是立刻执行,则会马上启动一次执行,如果是等待下次执行,则会忽略错过的任务,而等待下次(即第 6 分钟)触发执行。
读者可以在自己的项目中体会两种任务的区别以及 Quartz 的处理方法,根据具体情况选择不同类型的任务。
应用三:如何设置 Quartz 的线程池和并发任务
Quartz 中自带了一个线程池的实现:SimpleThreadPool。类如其名,这只是线程池的一个简单实现,没有提供动态自发调整等高级特性。Quartz 提供了一个配置参数:org.quartz.threadPool.threadCount,可以在初始化时设定线程池的线程数量,但是一次设定后不能再修改。假定这个数目是 10,则在并发任务达到 10 个以后,再有触发的任务就无法被执行了,只能等待有空闲线程的时候才能得到执行。因此有些 trigger 就可能被 misfire。但是必须指出一点,这个初始线程数并不是越大越好。当并发线程太多时,系统整体性能反而会下降,因为系统把很多时间花在了线程调度上。根据一般经验,这个值在 10 -- 50 比较合适。
对于一些注重性能的线程池来说,会根据实际线程使用情况进行动态调整,例如初始线程数,最大线程数,空闲线程数等。读者在应用中,如果有更好的线程池,则可以在配置文件中通过下面参数替换 SimpleThreadPool:org.quartz.threadPool.class = myapp.GreatThreadPool。
应用四:如何处理 Misfired 任务
在 Quartz 应用中,misfired job 是经常遇到的情况。一般来说,下面这些原因可能造成 misfired job:
1)系统因为某些原因被重启。在系统关闭到重新启动之间的一段时间里,可能有些任务会
被 misfire;
2)Trigger 被暂停(suspend)的一段时间里,有些任务可能会被 misfire;
3)线程池中所有线程都被占用,导致任务无法被触发执行,造成 misfire;
4)有状态任务在下次触发时间到达时,上次执行还没有结束;
为了处理 misfired job,Quartz 中为 trigger 定义了处理策略,主要有下面两种:
MISFIRE_INSTRUCTION_FIRE_ONCE_NOW:针对 misfired job 马上执行一次;
MISFIRE_INSTRUCTION_DO_NOTHING:忽略 misfired job,等待下次触发;
建议读者在应用开发中,将该设置作为可配置选项,使得用户可以在使用过程中,针对已经添加的 tirgger 动态配置该选项。
应用五:如何保留已经结束的 Trigger
在 Quartz 中,一个 tirgger 在最后一次触发完成之后,会被自动删除。Quartz 默认不会保留已经结束的 trigger,如下面 Quartz 源代码所示:

但是在实际应用中,有些用户需要保留以前的 trigger,作为历史记录,或者作为以后创建其他 trigger 的依据。如何保留结束的 trigger 呢?
一个办法是应用开发者自己维护一份数据备份记录,并且与 Quartz 原表的记录保持一定的同步。这个办法实际操作起来比较繁琐,而且容易出错,不推荐使用。
另外一个办法是通过修改并重新编译 Quartz 的 trigger 类,修改其默认的行为。我们以 org.quartz.SimpleTrigger 为例,修改上面代码中 if (!mayFireAgain()) 部分的代码如
另外我们需要在 SimpleTrigger 中定义一个新的类属性:needRetain

在定义自己的 trigger 时,设置该属性,就可以选择是否在 trigger 结束时删除 trigger。如下代码所示:

有人可能会考虑通过定义一个新的类,然后继承 org.quartz.SimpleTrigger 类并覆盖 executionComplete( ) 方法来实现。但是这种方法是行不通的,因为 Quartz 内部在处理时会根据 trigger 的类型重新生成 SimpleTrigger 类的实例,而不是使用我们自己定义的类创建的实例。这一点应该是 Quartz 的一个小小的不足之处,因为它把扩展 trigger 的能力堵死了。好在 Quartz 是开源的,我们可以根据需要进行修改。
回页首
小结
作为当前颇具生命力的开源框架,Quartz 已经得到了广泛的应用。Quartz 的强大功能和应用灵活性,在企业应用中发挥了巨大的作用。本文描述了如何应用 Quartz 开发应用程序,并对企业应用中常见的问题及解决方案进行了讨论。

以上转自https://www.ibm.com/developerworks/cn/opensource/os-cn-quartz/
----------------------------------------------------------------------------------
quartz 时间配置规则
格式: [秒] [分] [小时] [日] [月] [周] [年]

序号 说明
是否必填 允许填写的值 允许的通配符
1 秒 是 0-59   , - * /
2 分 是 0-59
  , - * /
3 小时 是 0-23   , - * /
4 日 是 1-31   , - * ? / L W
5 月 是 1-12 or JAN-DEC   , - * /
6 周 是 1-7 or SUN-SAT   , - * ? / L #
7 年 否 empty 或 1970-2099 , - * /
通配符说明:
* 表示所有值. 例如:在分的字段上设置 "*",表示每一分钟都会触发。
? 表示不指定值。使用的场景为不需要关心当前设置这个字段的值。例如:要在每月的10号触发一个操作,但不关心是周几,所以需要周位置的那个字段设置为"?" 具体设置为 0 0 0 10 * ?
- 表示区间。例如 在小时上设置 "10-12",表示 10,11,12点都会触发。
, 表示指定多个值,例如在周字段上设置 "MON,WED,FRI" 表示周一,周三和周五触发
/ 用于递增触发。如在秒上面设置"5/15" 表示从5秒开始,每增15秒触发(5,20,35,50)。在月字段上设置'1/3'所示每月1号开始,每隔三天触发一次。
L 表示最后的意思。在日字段设置上,表示当月的最后一天(依据当前月份,如果是二月还会依据是否是润年[leap]), 在周字段上表示星期六,相当于"7"或"SAT"。如果在"L"前加上数字,则表示该数据的最后一个。例如在周字段上设置"6L"这样的格式,则表示“本月最后一个星期五"
W 表示离指定日期的最近那个工作日(周一至周五). 例如在日字段上设置"15W",表示离每月15号最近的那个工作日触发。如果15号正好是周六,则找最近的周五(14号)触发, 如果15号是周未,则找最近的下周一(16号)触发.如果15号正好在工作日(周一至周五),则就在该天触发。如果指定格式为 "1W",它则表示每月1号往后最近的工作日触发。如果1号正是周六,则将在3号下周一触发。(注,"W"前只能设置具体的数字,不允许区间"-").
小提示
'L'和 'W'可以一组合使用。如果在日字段上设置"LW",则表示在本月的最后一个工作日触发(一般指发工资 )
# 序号(表示每月的第几个周几),例如在周字段上设置"6#3"表示在每月的第三个周六.注意如果指定"#5",正好第五周没有周六,则不会触发该配置(用在母亲节和父亲节再合适不过了)
小提示
周字段的设置,若使用英文字母是不区分大小写的 MON 与mon相同.

      
常用示例:

0 0 12 * * ? 每天12点触发
0 15 10 ? * * 每天10点15分触发
0 15 10 * * ? 每天10点15分触发
0 15 10 * * ? * 每天10点15分触发
0 15 10 * * ? 2005 2005年每天10点15分触发
0 * 14 * * ? 每天下午的 2点到2点59分每分触发
0 0/5 14 * * ? 每天下午的 2点到2点59分(整点开始,每隔5分触发)
0 0/5 14,18 * * ?
每天下午的 2点到2点59分(整点开始,每隔5分触发)
每天下午的 18点到18点59分(整点开始,每隔5分触发)
0 0-5 14 * * ? 每天下午的 2点到2点05分每分触发
0 10,44 14 ? 3 WED 3月分每周三下午的 2点10分和2点44分触发
0 15 10 ? * MON-FRI 从周一到周五每天上午的10点15分触发
0 15 10 15 * ? 每月15号上午10点15分触发
0 15 10 L * ? 每月最后一天的10点15分触发
0 15 10 ? * 6L 每月最后一周的星期五的10点15分触发
0 15 10 ? * 6L 2002-2005 从2002年到2005年每月最后一周的星期五的10点15分触发
0 15 10 ? * 6#3 每月的第三周的星期五开始触发
0 0 12 1/5 * ? 每月的第一个中午开始每隔5天触发一次
0 11 11 11 11 ? 每年的11月11号 11点11分触发(光棍节)

猜你喜欢

转载自haidaoqi3630.iteye.com/blog/2203735
今日推荐