Quartz-java定时任务

Java
324
0
0
2023-06-12
标签   Java任务

描述

强大的调度功能,例如支持丰富多样的调度方法,可以满足各种常规及特殊需求;

灵活的应用方式,例如支持任务和调度的多种组合方式,支持调度数据的多种存储方式;

分布式和集群能力,Terracotta 收购后在原来功能基础上作了进一步提升。

说明:

scheduler:

任务调度器

trigger:

触发器,用于定义任务调度时间规则

job:

任务,即被调度的任务

misfire:

错过的,指本来应该被执行但实际没有被执行的任务调度。

强大的调度功能,例如支持丰富多样的调度方法,可以满足各种常规及特殊需求; 灵活的应用方式,例如支持任务和调度的多种组合方式,支持调度数据的多种存储方式; 分布式和集群能力,Terracotta 收购后在原来功能基础上作了进一步提升。另外,作为 Spring 默认的调度框架, Quartz 很容易与 Spring 集成实现灵活可配置的调度功能。

quartz元素介绍

Quartz 任务调度的核心元素是 scheduler, trigger 和 job, 其中 trigger 和 job 是任务调度的元数据, scheduler 是实际执行调度的控制器。

Trigger

在 Quartz 中,trigger 是用于定义调度时间的元素,即按照什么时间规则去执行任务。Quartz 主要提供了四种类型的 trigger: SimpleTrigger CronTirgger DateIntervalTrigger ,和 NthIncludedDayTrigger 。这四种类型 trigger 可以满足企业应用中的绝大部分需求。我们将在企业应用一节中进一步讨论四种类型 trigger 的功能。

job

在 Quartz 中,job 用于表示被调度的任务。主要有两种类型的 job:无状态的(stateless)和有状态的(stateful)。对于同一个 trigger 来说,有状态的 job 不能被并行执行,只有上一次触发的任务被执行完之后,才能触发下一次执行。Job 主要有两种属性:volatility 和 durability,其中 volatility 表示任务是否被 持久化 到数据库存储,而 durability 表示在没有 trigger 关联的时候任务是否被保留。两者都是在值为 true 的时候任务被持久化或保留。一个 job 可以被多个 trigger 关联,但是一个 trigger 只能关联一个 job。

scheduler

在 Quartz 中, scheduler 由 scheduler 工厂创建:DirectSchedulerFactory 或者 StdSchedulerFactory。

第二种工厂 StdSchedulerFactory 使用较多,因为 DirectSchedulerFactory 使用起来不够方便,需要作许多详细的手工编码设置。

Scheduler 主要有三种:RemoteMBeanScheduler, RemoteScheduler 和 StdScheduler。本文以最常用的 StdScheduler 为例讲解。这也是笔者在项目中所使用的 scheduler 类。

Quartz 核心元素之间的关系如下图所示:

Quartz-java定时任务 Quartz 核心元素之间的关系

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

图 2. Quartz 线程视图

Quartz-java定时任务 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 以实现这些特殊处理。Quartz 已经自带了一些数据库的扩展实现,可以直接使用,如下图所示:

Quartz-java定时任务 StdJDBCDelegate

图 4. Quartz 数据库驱动代理

作为嵌入式数据库的代表, Derby 近来非常流行。如果使用 Derby 数据库,可以使用上图中的 CloudscapeDelegate 作为 trigger 和 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 执行任务。

cron表达式语法: [秒] [分] [小时] [日] [月] [周] [年]

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 接口的任务为有状态的,下图列出了 Quartz 中 Job 接口的定义以及一些自带的实现类:

Quartz-java定时任务

图 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 的线程池和并发任务

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。

表名称

说明

qrtz_ Blob _triggers

Trigger作为Blob类型存储(用于Quartz用户用JDBC创建他们自己定制的Trigger类型,JobStore 并不知道如何存储实例的时候)

qrtz_calendars

以Blob类型存储Quartz的Calendar日历信息, quartz可配置一个日历来指定一个时间范围

qrtz_cron_triggers

存储Cron Trigger,包括Cron表达式和时区信息。

qrtz_fired_triggers

存储与已触发的Trigger相关的状态信息,以及相联Job的执行信息

qrtz_job_details

存储每一个已配置的Job的详细信息

qrtz_locks

存储程序的悲观锁的信息(假如使用了悲观锁)

qrtz_paused_trigger_graps

存储已暂停的Trigger组的信息

qrtz_scheduler_state

存储少量的有关 Scheduler的状态信息,和别的 Scheduler 实例(假如是用于一个集群中)

qrtz_simple_triggers

存储简单的 Trigger,包括重复次数,间隔,以及已触的次数

qrtz_triggers

存储已配置的 Trigger的信息

qrzt_simprop_triggers


核心类解释说明

(1)核心类 QuartzSchedulerThread :负责执行向QuartzScheduler注册的触发Trigger的工作的线程。

ThreadPool :Scheduler使用一个线程池作为任务运行的基础设施,任务通过共享线程池中的线程提供运行效率。

QuartzSchedulerResources :包含创建QuartzScheduler实例所需的所有资源(JobStore,ThreadPool等)。

SchedulerFactory :提供用于获取调度程序实例的客户端可用句柄的机制。

JobStore : 通过类实现的接口,这些类要为org.quartz.core.QuartzScheduler的使用提供一个org.quartz.Job和org.quartz.Trigger存储机制。作业和触发器的存储应该以其名称和组的组合为唯一性。 QuartzScheduler :这是Quartz的核心,它是org.quartz.Scheduler接口的间接实现,包含调度org.quartz.Jobs,注册org.quartz.JobListener实例等的方法。

Scheduler :这是Quartz Scheduler的主要接口,代表一个独立运行容器。调度程序维护JobDetails和触发器的注册表。 一旦注册,调度程序负责执行作业,当他们的相关联的触发器触发(当他们的预定时间到达时)。

Trigger :具有所有触发器通用属性的基本接口,描述了job执行的时间触发规则。 – 使用TriggerBuilder实例化实际触发器。

JobDetail : 传递给定作业实例的详细信息属性。 JobDetails将使用JobBuilder创建/定义。

Job: 要由表示要执行的“作业”的类实现的接口。只有一个方法 void execute(jobExecutionContext context) (jobExecutionContext 提供调度上下文各种信息,运行时数据保存在jobDataMap中) Job有个子接口StatefulJob ,代表有状态任务。有状态任务不可并发,前次任务没有执行完,后面任务处于阻塞等待。

其他知识点

quartz 实际并不关心你是在相同的还是不同的机器上运行节点。 当集群是放置在不同的机器上时,通常称之为水平集群。节点是跑在同一台机器是,称之为垂直集群。

配置属性介绍

org.quartz.scheduler.instanceName: 属性可为任何值,用在 JDBC JobStore 中来唯一标识实例,但是所有集群节点中必须相同。

org.quartz.scheduler.instanceId: 属性为 AUTO即可,基于主机名和时间戳来产生实例 ID。

org.quartz.jobStore.class: 属性为 JobStoreTX,将任务持久化到数据中。因为集群中节点依赖于数据库来传播 Scheduler 实例的状态,你只能在使用 JDBC JobStore 的应用 Quartz 集群。 这意味着你必须使用 JobStoreTX 或是 JobStoreCMT 作为 Job 存储;你不能在集群中使用 RAMJobStore。

org.quartz.jobStore.isClustered: 属性为 true, Scheduler 实例要它参与到一个集群当中。 这一属性会贯穿于调度框架的始终,用于修改集群环境中操作的默认行为。

org.quartz.jobStore.clusterCheckinInterval: 属性定义了Scheduler 实例检入到数据库中的频率(单位:毫秒)。 Scheduler 检查是否其他的实例到了它们应当检入的时候未检入;这能指出一个失败的 Scheduler 实例,且当前 Scheduler 会以此来接管任何执行失败并可恢复的 Job。 通过检入操作,Scheduler 也会更新自身的状态记录。clusterChedkinInterval 越小,Scheduler 节点检查失败的 Scheduler 实例就越频繁。默认值是 15000 (即15 秒)。