一、Quartz基本概念
Quartz 是 OpenSymphony开源组织在任务调度领域的一个开源项目,完全基于 Java 实现。该项目于 2009 年被 Terracotta 收购,目前是 Terracotta 旗下的一个项目。读者可以到 http://www.quartz-scheduler.org/站点下载 Quartz 的最新发布版本及其源代码。QCT系统使用的是版本 1.6.0,因此本文内容基于该版本。本文不仅介绍如何应用 Quartz 进行开发,也对其内部实现原理作一定讲解。
二、Quartz的特点


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

Scheduler调度线程主要有两个:执行常规的线程和执行misfired trigger的线程。常规调度线程轮询存储的所有Trigger,如果有需要触发的trigger,即到达了下一个触发时间,则从任务执行线程池中获取一个空闲线程,执行与该trigger关联的任务。Misfire线程是扫描所有的trigger,查看是否有misfired trigger,如果有就根据misfire的策略分别处理。下图描述了这两个线程的基本流程


以上是run的最开头的一段,不难看出这是在等待scheduler的start,实际上Quartz就是通过线程的wait或sleep来实现时间调度。继续看代码:

这段代码是从jobStore里拿到下一个要执行的trigger,一般情况下jobStore使用的是RAMJobStore,即trigger等相关信息存放在内存里,如果需要把任务持久化就得使用可持久化JobStore。继续看代码:

此段代码是计算下一个trigger的执行时间和现在系统时间的差,然后通过循环线程sleep的方式暂停住此线程,一直等到trigger的执行时间点。继续看代码:

此段代码就是包装trigger,然后通过以JobRunShell为载体,在threadpool里执行trigger所关联的jobDetail。

Quartz数据表

七、 Quartz与Spring集成

第一种方式的Java代码:

方法二、借助于Spring的org.springframework.scheduling.quartz.JobDetailBean的类功能,继承 Spring封装Quartz的org.springframework.scheduling.quartz.QuartzJobBean类,实现 executeInternal方法,executeInternal方法中调用业务类。

第二种方式的Java代码:

八、 企业级开发中常见应用
在应用Quartz进行企业级的开发是,有些问题会经常遇到。下面介绍企业开发中常见的一些问题及通常的解决办法:
应用一:如何使用不同类型的Trigger
前面我们提到Quartz中三种类型的Trigger:SimpleTrigger,CronTrigger,NthIncludedDayTrigger。
SimpleTrigger一般用于实现每隔一定时间执行任务,以及重复多少次,如每2小时执行一次,重复执行5次。SimpleTrigger内部实现机制是通过计算间隔时间来计算下次的执行时间,这就导致其不合适调度定时的任务。例如我们想每天的8:00AM执行任务,如果使用SimpleTrigger的话间隔时间就是一天。主要这里就有一个问题,即当有misfired的任务并且恢复执行时,该执行时间是随机的(取决于何时执行misfired的任务,例如某天的9:00PM)。这会导致之后每天的执行时间都会变成9:00PM,而不是我原来期望的8:00AM。
CronTrigger类似于Linux上的任务调度命令crontab,即利用一个包含7个字段的表达式来表示时间调度方式。例如,“0 15 10 * * ? *”表示每天的10:15AM执行任务。对于涉及到星期和月份的调度,CronTrigger是最合适的,甚至某些情况下是唯一选择。例如,“0 10 14 ? 3 WED”表示三月份的每个星期三的下午14:10PM执行任务。使用者可以在具体用的该trigger时再详细了解每个字段的含义。
NthIncludedDayTrigger的用途比较简单明确,即用于每间隔一个周期的第几天调度任务,例如,每个月的第2天执行指定的任务。
应用二:使用有状态(StatefulJob)还是无状态的任务(Job)
在Quartz中,Job是一个接口,企业应用需要实现这个接口定义自己的任务。基本来说,任务分为有状态和无状态两种。实现Job接口的任务缺省为无状态的。Quartz中还有另外一个接口StatefulJob。实现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的线程池和并发任务
应用四:如何处理Misfired任务
在Quartz应用中,misfired job是经常遇到的情况,一般来说,下面这些原因可能造成misfired job:
1)系统因为某些原因被重启。在系统关闭到重启之间的一段时间里,可能有些任务会被misfire;
2)Trigger被暂停(pause)的一段时间里,有些任务可能会被misfire;
3)线程池中所有线程都被占用,导致任务无法被触发执行,造成misfire;
4)有状态任务在下次触发时间到达时,上次执行还没有结束;
为了处理misfired job,Quartz中为trigger定义了处理策略,主要有下面两种:
MISFIRE_INSTRUCTION_FIRE_ONCE_NOW:针对 misfired job 马上执行一次;
MISFIRE_INSTRUCTION_DO_NOTHING:忽略 misfired job,等待下次触发。