再识Quartz
在之前的项目中使用过Quartz,但都是基于XML配置定义任务的。目前一个项目应用需要对任务进行创建、暂停、删除等动态管理。所以再次在网上翻了翻,再来好好重新认识下Quartz。
名词解释:
Quartz 任务调度的基本实现原理
核心元素
Quartz 任务调度的核心元素是 scheduler, trigger 和 job,其中 trigger 和 job 是任务调度的元数据, scheduler 是实际执行调度的控制器。
在 Quartz 中,trigger 是用于定义调度时间的元素,即按照什么时间规则去执行任务。Quartz 中主要提供了四种类型的 trigger:SimpleTrigger,CronTirgger,DateIntervalTrigger,和 NthIncludedDayTrigger。
在 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。
Quartz 核心元素之间的关系如下图所示:
图 1. Quartz 核心元素关系图
线程视图
在 Quartz 中,有两类线程,Scheduler 调度线程和任务执行线程,其中任务执行线程通常使用一个线程池维护一组线程。
图 2. Quartz 线程视图
Scheduler 调度线程主要有两个: 执行常规调度的线程,和执行 misfired trigger 的线程。常规调度线程轮询存储的所有 trigger,如果有需要触发的 trigger,即到达了下一次触发的时间,则从任务执行线程池获取一个空闲线程,执行与该 trigger 关联的任务。Misfire 线程是扫描所有的 trigger,查看是否有 misfired trigger,如果有的话根据 misfire 的策略分别处理。
数据存储
Quartz 中的 trigger 和 job 需要存储下来才能被使用。Quartz 中有两种存储方式:RAMJobStore, JobStoreSupport,其中 RAMJobStore 是将 trigger 和 job 存储在内存中,而 JobStoreSupport 是基于 jdbc 将 trigger 和 job 存储到数据库中。RAMJobStore 的存取速度非常快,但是由于其在系统被停止后所有的数据都会丢失,所以在通常应用中,都是使用 JobStoreSupport。
在 Quartz 中,JobStoreSupport 使用一个驱动代理来操作 trigger 和 job 的数据存储:StdJDBCDelegate。StdJDBCDelegate 实现了大部分基于标准 JDBC 的功能接口,但是对于各种数据库来说,需要根据其具体实现的特点做某些特殊处理,因此各种数据库需要扩展 StdJDBCDelegate 以实现这些特殊处理。
开发中的常见问题
一:如何使用不同类型的 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 接口的定义以及一些自带的实现类:
图 14. 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 中自带了一个线程池的实现: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 动态配置该选项。
相关资料引用:
https://www.ibm.com/developerworks/cn/opensource/os-cn-quartz/