基于 Quartz 开发企业级任务调度应用.docx
《基于 Quartz 开发企业级任务调度应用.docx》由会员分享,可在线阅读,更多相关《基于 Quartz 开发企业级任务调度应用.docx(13页珍藏版)》请在冰豆网上搜索。
![基于 Quartz 开发企业级任务调度应用.docx](https://file1.bdocx.com/fileroot1/2023-1/4/c27c71d7-2fde-472d-a64b-8c937d9f8aa3/c27c71d7-2fde-472d-a64b-8c937d9f8aa31.gif)
基于Quartz开发企业级任务调度应用
Quartz是OpenSymphony开源组织在任务调度领域的一个开源项目,完全基于Java实现。
作为一个优秀的开源调度框架,Quartz具有功能强大,应用灵活,易于集成的特点。
本文剖析了Quartz框架内部的基本实现原理,通过一些具体实例描述了应用Quartz开发应用程序的基本方法,并对企业应用中常见的问题及解决方案进行了讨论。
Quartz基本概念及原理
QuartzScheduler开源框架
Quartz是OpenSymphony开源组织在任务调度领域的一个开源项目,完全基于Java实现。
该项目于2009年被Terracotta收购,目前是Terracotta旗下的一个项目。
读者可以到 http:
//www.quartz-scheduler.org/站点下载Quartz的发布版本及其源代码。
笔者在产品开发中使用的是版本1.8.4,因此本文内容基于该版本。
本文不仅介绍如何应用Quartz进行开发,也对其内部实现原理作一定讲解。
作为一个优秀的开源调度框架,Quartz具有以下特点:
1.强大的调度功能,例如支持丰富多样的调度方法,可以满足各种常规及特殊需求;
2.灵活的应用方式,例如支持任务和调度的多种组合方式,支持调度数据的多种存储方式;
3.分布式和集群能力,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核心元素之间的关系如下图所示:
图1.Quartz核心元素关系图
线程视图
在Quartz中,有两类线程,Scheduler调度线程和任务执行线程,其中任务执行线程通常使用一个线程池维护一组线程。
图2.Quartz线程视图
Scheduler调度线程主要有两个:
执行常规调度的线程,和执行misfiredtrigger的线程。
常规调度线程轮询存储的所有trigger,如果有需要触发的trigger,即到达了下一次触发的时间,则从任务执行线程池获取一个空闲线程,执行与该trigger关联的任务。
Misfire线程是扫描所有的trigger,查看是否有misfiredtrigger,如果有的话根据misfire的策略分别处理。
下图描述了这两个线程的基本流程:
图3.Quartz调度线程流程图
关于misfiredtrigger,我们在企业应用一节中将进一步描述。
数据存储
Quartz中的trigger和job需要存储下来才能被使用。
Quartz中有两种存储方式:
RAMJobStore,JobStoreSupport,其中RAMJobStore是将trigger和job存储在内存中,而JobStoreSupport是基于jdbc将trigger和job存储到数据库中。
RAMJobStore的存取速度非常快,但是由于其在系统被停止后所有的数据都会丢失,所以在通常应用中,都是使用JobStoreSupport。
在Quartz中,JobStoreSupport使用一个驱动代理来操作trigger和job的数据存储:
StdJDBCDelegate。
StdJDBCDelegate实现了大部分基于标准JDBC的功能接口,但是对于各种数据库来说,需要根据其具体实现的特点做某些特殊处理,因此各种数据库需要扩展StdJDBCDelegate以实现这些特殊处理。
Quartz已经自带了一些数据库的扩展实现,可以直接使用,如下图所示:
图4.Quartz数据库驱动代理
作为嵌入式数据库的代表,Derby近来非常流行。
如果使用Derby数据库,可以使用上图中的CloudscapeDelegate作为trigger和job数据存储的代理类。
基本开发流程及简单实例
搭建开发环境
利用Quartz进行开发相当简单,只需要将下载开发包中的quartz-all-1.8.4.jar加入到classpath即可。
根据笔者的经验,对于任务调度功能比较复杂的企业级应用来说,最好在开发阶段将Quartz的源代码导入到开发环境中来。
一方面可以通过阅读源码了解Quartz的实现机理,另一方面可以通过扩展或修改Quartz的一些类来实现某些Quartz尚不提供的功能。
图5.Quartz实例工程及源码导入
上图中左边是源码导入后的截图,其中org.quartz.*即为quartz的源码。
导入源码后可能会有一些编译错误,通常出现在org.quartz.ee.*和org.quartz.jobs.ee.*包中。
下载开发包中有一个lib目录,读者可以将该目录下的jar文件加入到编译环境。
如果还有编译错误,读者可以参考上图中右侧的jar列表,到网上去搜索下载。
项目中com.ibm.zxn.sample.quartz是我们自己的类包,下面的实例中我们会用到它。
一个简单实例
Quartz开发包中有一个examples目录,其中有15个基本实例。
建议读者阅读并实践这些例子。
本文这里只列举一个小的实例,介绍基本的开发方法。
1.准备数据库和Quartz用的数据表
1.本文使用IBMDB2数据库:
将jdbc驱动程序db2jcc.jar加入到项目中;
2.在数据库中创建一个新库QUARTZDB;
3.执行/quartz-1.8.4/docs/dbTables/tables_db2_v8.sql,创建数据表;表建好后如下所示:
图6.Quartz数据表
2.准备配置文件,加入到项目中
图7.实例配置文件
3.通过实现job接口定义我们自己的任务类,如下所示:
图8.定义任务类
4.然后,实现任务调度的主程序,如下所示:
本实例中,我们利用DateIntervalTrigger实现一个每两分钟执行一次的任务调度。
图9.实现主程序
5.完成后项目结构如下所示:
图10.实例项目结构图
6.运行程序,查看数据库表和运行结果
数据库中,QRTZ_TRIGGERS表中添加了一条trigger记录,如下所示:
图11.QRTZ_TRIGGERS表中的记录
QRTZ_JOB_DETAILS表中添加了一条job记录,如下所示:
图12.QRTZ_JOB_DETAILES表中的记录
从运行结果来看,任务每两分钟被执行一次:
图13.运行结果
企业级开发中的常见应用
在应用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个字段的表达式来表示时间调度方式。
例如,"01510**?
*"表示每天的10:
15AM执行任务。
对于涉及到星期和月份的调度,CronTirgger是最适合的,甚至某些情况下是唯一选择。
例如,"01014?
3WED"表示三月份的每个星期三的下午14:
10PM执行任务。
读者可以在具体用到该trigger时再详细了解每个字段的含义。
DateIntervalTrigger是Quartz1.7之后的版本加入的,其最适合调度类似每N(1,2,3...)小时,每N天,每N周等的任务。
虽然SimpleTrigger也能实现类似的任务,但是DateIntervalTrigger不会受到我们上面说到的misfired任务的影响。
另外,DateIntervalTrigger也不会受到DST(DaylightSavingTime,即中国的夏令时)调整的影响。
笔者就曾经因为该原因将项目中的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接口的定义以及一些自带的实现类:
图14.Quartz中Job接口定义
无状态任务一般指可以并发的任务,即任务之间是独立的,不会互相干扰。
例如我们定义一个trigger,每2分钟执行一次,但是某些情况下一个任务可能需要3分钟才能执行完,这样,在上一个任务还处在执行状态时,下一次触发时间已经到了。
对于无状态任务,只要触发时间到了就会被执行,因为几个相同任务可以并发执行。
但是对有状态任务来说,是不能并发执行的,同一时间只能有一个任务在执行。
在笔者项目中,某些任务需要对数据库中的数据进行增删改处理。
这些任务不能并发执行,否则会造成数据混乱。
因此我们使用StatefulJob接口。
现在回到上面的例子,任务每2分钟执行一次,若某次任务执行了5分钟才完成,Quartz会怎么处理呢?
按照trigger的规则,第2分钟和第4分钟分别会有一次预定的触发执行,但是由于是有状态任务,因此实际不会被触发。
在第5分钟第一次任务执行完毕时,Quartz会把第2和第4分钟的两次触发作为misfiredjob进行处理。
对于misfiredjob,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应用中,misfiredjob是经常遇到的情况。
一般来说,下面这些原因可能造成misfiredjob:
1)系统因为某些原因被重启。
在系统关闭到重新启动之间的一段时间里,可能有些任务会
被misfire;
2)Trigger被暂停(suspend)的一段时间里,有些任务可能会被misfire;
3)线程池中所有线程都被占用,导致任务无法被触发执行,造成misfire;
4)有状态任务在下次触发时间到达时,上次执行还没有结束;
为了处理misfiredjob,Quartz中为trigger定义了处理策略,主要有下面两种:
MISFIRE_INSTRUCTION_FIRE_ONCE_NOW:
针对misfiredjob马上执行一次;
MISFIRE_INSTRUCTION_DO_NOTHING:
忽略misfiredjob,等待下次触发;
建议读者在应用开发中,将该设置作为可配置选项,使得用户可以在使用过程中,针对已经添加的tirgger动态配置该选项。
应用五:
如何保留已经结束的Trigger
在Quartz中,一个tirgger在最后一次触发完成之后,会被自动删除。
Quartz默认不会保留已经结束的trigger,如下面Quartz源代码所示:
图15.executionComplete()源码
但是在实际应用中,有些用户需要保留以前的trigger,作为历史记录,或者作为以后创建其他trigger的依据。
如何保留结束的trigger呢?
一个办法是应用开发者自己维护一份数据备份记录,并且与Quartz原表的记录保持一定的同步。
这个办法实际操作起来比较繁琐,而且容易出错,不推荐使用。
另外一个办法是通过修改并重新编译Quartz的trigger类,修改其默认的行为。
我们以org.quartz.SimpleTrigger为例,修改上面代码中if(!
mayFireAgain())部分的代码如下:
图16.修改executionComplete()源码
另外我们需要在SimpleTrigger中定义一个新的类属性:
needRetain,如下所示:
图17.定义新属性needRetain
在定义自己的trigger时,设置该属性,就可以选择是否在trigger结束时删除trigger。
如下代码所示:
图18.使用修改后的SimpleTrigger
有人可能会考虑通过定义一个新的类,然后继承org.quartz.SimpleTrigger类并覆盖executionComplete()方法来实现。
但是这种方法是行不通的,因为Quartz内部在处理时会根据trigger的类型重新生成SimpleTrigger类的实例,而不是使用我们自己定义的类创建的实例。
这一点应该是Quartz的一个小小的不足之处,因为它把扩展trigger的能力堵死了。
好在Quartz是开源的,我们可以根据需要进行修改。
小结
作为当前颇具生命力的开源框架,Quartz已经得到了广泛的应用。
Quartz的强大功能和应用灵活性,在企业应用中发挥了巨大的作用。
本文描述了如何应用Quartz开发应用程序,并对企业应用中常见的问题及解决方案进行了讨论。