Quartz

1. 简介

Quartz 是个任务调度工具,就是定时执行指定的任务。

Quartz 提供了极为广泛的特性如持久化任务,集群和分布式任务等,用 Java 写成,与 Spring 集成方便,伸缩性,负载均衡,高可用性。

本文只关注基于数据库的 Quartz 集群,基于 Quartz 2.2.3 来说明。

2. 核心类

2.1. org.quartz.Job

任务接口,任务类代表要调度执行的业务逻辑实现,必须实现该接口。

如果任务不允许并发执行,则任务类必须添加注解 @DisallowConcurrentExecution 。

该接口只定义了一个方法:
void execute(JobExecutionContext context) throws JobExecutionException;

通过 context 可以访问配置给任务的数据 context.getJobDetail().getJobDataMap(),如果这个 JobDataMap 需要在修改后持久化到数据库里,则需要给任务类加上注解 @PersistJobDataAfterExecution 。Quartz 在下次调度执行这个任务时,会把持久化的数据反序列化成 JobDataMap 供应用使用。

不建议通过 Quartz 的这个机制持久化任何与业务有关的数据。因为业务数据应该由应用来存储,Quartz 只关注于调度执行。

2.2. org.quartz.JobDetail

任务在内存的表示。表示任务详细信息的接口,任务用调度器名称、任务名称和任务所归属的组名 来做唯一标识。

2.3. org.quartz.Trigger

触发器。用调度器名称、触发器名称和触发器所归属的组名 来做唯一标识。用于定义在什么情况、什么时间点执行任务。最常用的触发器类型就是基于 cron 表达式的。

2.4. org.quartz.spi.JobStore

数据存储抽象。该接口定义了 任务、触发器的存储、检索、更新 钩子,该接口还定义了任务执行完成后的回调方法。

Quartz 内建支持把任务、触发器等数据存储在JVM内存、文件、数据库,通过这个接口,就隔离了底层存储机制的差异。可以在配置文件里通过 org.quartz.jobStore.class 属性来指定该接口的实现,比如基于 Redis 做数据存储的实现。

2.5. org.quartz.impl.jdbcjobstore.JobStoreSupport

提供了基于数据库的 JobStore 实现、通过 MisfireHandler 来检查错过发射的触发器,如果是集群方式部署,还会通过内部类 ClusterManager 提供集群健康检查与恢复。

2.6. org.quartz.spi.JobFactory

接口抽象了如何生成任务的实例,以便让应用来决定如何实例化任务类。

2.7. 集群管理线程

JobStoreSupport 有个内部类 ClusterManager 用于进行集群管理,它用独立的线程来执行,防止任务阻塞导致它延迟执行。核心逻辑有:
1、 签入,以向其他节点传达它所在的调度器实例还存活。
2、 检查失败的节点,进行恢复。Quartz 认为在一定时间后没有签入的节点是失败的,需要恢复。

2.8. MisfireHandler

JobStoreSupport 有个内部类 MisfireHandler 用于检查是否有触发器错过发射,它用独立的线程执行。

但检测到有触发器错过发射时,该处理器只是更新触发器的状态为 WAITING,然后通知监听器、通知调度器去处理。

2.9. 工作线程池

Quartz 可以配置一个线程池来执行任务,线程池里的线程用完后,后续到期需要执行的任务就会被阻塞。

2.10. 调度器线程

每个调度器都有自己的名称, 对应一个 org.quartz.core.QuartzSchedulerThread 调度器线程实例,调度逻辑就在这个线程类的 run 方法里。

3. Java 类、任务、触发器的关系

一个实现了 org.quartz.Job 接口的 Java 类可以定义成 Quartz 的多个任务,这些任务在 JOB_DETAILS 表里的 JOB_CLASS_NAME 是相同的,但是 SCHED_NAME,JOB_NAME,JOB_GROUP 不同。

一个类可以定义成多个任务,一个任务可以由多个触发器来触发执行。任务、触发器总是与某个的调度器关联的。

建议:一个类只对应一个任务,一个任务只对应一个触发器。

4. 核心的表

job_details:存储任务的信息,每一条记录表示一个任务。

triggers:存储触发器的信息,TRIGGER_STATE 字段表示记录的状态,用来控制这个触发器能不能被调度器处理。

fired_triggers:已发射触发器的记录表,STATE 列用来表示任务的执行状态。该表的作用是跟踪任务的执行进度,用于失败处理。

locks:里面的每条记录作为一个悲观锁,要加锁时,用 for update 语句锁住对应的记录。

scheduler_state:调度器状态表,集群的节点通过这个表来检查其他节点是否存活。

5. 集群与并发控制

如果是基于本地内存存储任务、触发器信息,就不是集群的方式了,并发可以通过Java的同步机制来控制。以下是基于数据库的集群方式。

集群的节点通过数据库来感知其他节点。

下面是正常获取、触发任务执行的流程:

调度器线程执行的时候,首先从 triggers 表获取状态为 WAITING 的将要发射的触发器,然后条件更新状态 TRIGGER_STATE 的值为 ACQUIRED ,更新成功则表示抢占到了,否则可能是被其他调度器抢占。然后插入触发器信息及实例名到 FIRED_TRIGGERS 表。前面的更新与插入操作是在同一个事务里完成。

抢占到触发器后,调度器线程等待触发时间到来。

执行时间到来后,调度器线程首先会把 FIRED_TRIGGERS 表里触发器记录的状态更新为 EXECUTING ,如果任务允许并发执行,把 TRIGGERS 表里的状态更新为 WAITING , PAUSED 或 COMPLETE (不需要再执行的) ;如果任务不允许并发执行,还会把 TRIGGERS 表里的状态更新为 BLOCKED 或 PAUSED_BLOCKED,这个更新是根据 任务名和任务所属组名 而不是触发器名称和触发器所属组名 来更新的,这就解决了一个任务有多个触发器的并发问题;然后调度器线程会创建一个执行环境来执行任务,以便在任务执行完成后更新触发器的状态。任务执行完成后,在一个事务里把 triggers 里的触发器状态更新为 WAITING,删除 FIRED_TRIGGERS 表里对应的记录。

6. 集群故障恢复

集群故障恢复的逻辑在 JobStoreSupport 方法的 clusterRecover 方法里。它的逻辑是对每个失败的实例做如下处理:
1、 根据实例名和调度器名从 FIRED_TRIGGERS 表找出触发器信息,这些是要做恢复的触发器。
2、 对于上面找出的每一个触发器:
a) 更新 TRIGGERS 表里的状态: BLOCKED 更新为 WAITING , PAUSED_BLOCKED 更新为 PAUSED 、 ACQUIRED 更新为 WAITING 。
b) 对于触发器的状态为非 ACQUIRED 的、且任务要求 recovery 的,插入一条临时的触发器来触发任务执行。
c) 对于不允许并发执行的任务,根据 任务名、任务组名 等来更新 TRIGGERS 的状态:BLOCKED 更新为 WAITING 、 PAUSED_BLOCKED 更新为 WAITING 。这一步是为了恢复这个任务的其他触发器。
3、 根据实例名和调度器名从 FIRED_TRIGGERS 表统一删除触发器。
4、 对于上面找出的每一个触发器,如果它在 TRIGGERS 表里的状态是 COMPLETE 、且在 FIRED_TRIGGERS 表里没有记录了,则从 TRIGGERS 表删除。
5、 最后从 SCHEDULER_STATE 删除实例信息。


欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据