对Quartz的学习和研究

简要

Quartz是一个用java编写的job调度框架。
支持数据库,也就是说它可以连接数据库…
支持集群部署,您可以同时在多台机器上部署一个Quartz实例,job数量少时集群部署可以提高可用性…
支持cron表达式(此条是真的)…

尝试使用Quartz

程序员言道:”show me the code”,说得就是在学习任何工具框架之前,最好先跑一跑代码,对框架有第一印象之后再学习。Quartz是个job调度器,没有使用过它的童鞋一时半会儿无法理解调度器,因为job调度是个领域,对该领域没个了解是不行的。

简单地理解,Quartz运行的时候有3部分:Job、触发器和调度器。Job是我们自己写的逻辑代码,比如说发送一封秘密邮件。触发器,配置了Job什么时候执行。调度器,是调度框架的核心,没有它,Job和触发器没有意义。总而言之,言而总之,调度器根据触发器的配置,在特定的条件下『触发』特定的动作,比如我们设定触发器在每天凌晨1点触发我们的发邮件Job,调度器会在每天1点定时启动Job。

下面试调度一个Job

自己写的Job,通常业务逻辑都放在这里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.iloveqyc.service.test.quartz;

import com.iloveqyc.util.JsonUtils;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

import java.util.Date;

/**
* Project: rcs-web
* PACKAGE_NAME: com.iloveqyc.service.test.quartz
* Date: 2018/1/18
* Time: 下午2:54
*
* @author qiuyongchen David
* <p>
* Usage: xxx
*/
public class QycJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
System.out.println(JsonUtils.toStr(new Date()));
}
}

Job调度框架,逻辑相对比较固定,不涉及业务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.iloveqyc.service.test.quartz;

import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;

/**
* Project: rcs-web
* PACKAGE_NAME: com.iloveqyc.service.test.quartz
* Date: 2018/1/18
* Time: 上午11:44
*
* @author qiuyongchen David
* <p>
* Usage: xxx
*/
public class QycTest {
public static void main(String[] args) throws SchedulerException {

// 新建一个触发器
Trigger trigger = new SimpleTrigger("每秒执行一次共执行5次", 5 - 1, 1000);

// 新建一个任务
JobDetail jobDetail = new JobDetail("qyc_job_detail", QycJob.class);

// 新建一个调度器工厂
SchedulerFactory factory = new StdSchedulerFactory();
// 获取一个调度器(核心对象)
Scheduler scheduler = factory.getScheduler();
scheduler.scheduleJob(jobDetail, trigger);

// 开始调度job!
scheduler.start();
}
}

扫一眼上边的代码,有童鞋会想起Timer或ScheduledThreadPoolExecutor,没错╮(╯_╰)╭,Quartz就是一个类似于定时器的框架,使用Quartz的同学,只需要配置cron表达式和Job逻辑,不用亲自编写定时器。

Job & JobDetail

在Quartz中,Job用于存放业务的逻辑,回顾下我们是如何编写Job的:

1
2
3
4
5
6
7
8
9
10
11
public class QycJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {

// 业务代码开始点
// …
// …
// 业务代码结束点

}
}

可以看到,Job在运行的时候,Quartz向它传入JobExecutionContext,运行上下文,里边存放着Job运行时的信息,比如Job的名称,Job的执行情况等。

再回顾下我们怎么运行一个Job:

1
2
3
4
5
// 新建一个任务
JobDetail jobDetail = new JobDetail("qyc_job_detail", QycJob.class);
// …
// …
scheduler.scheduleJob(jobDetail, trigger);

我们把Job放入JobDetail,再把JobDetail传给调度器,相当于把Job封装了一层,看下官方源码声明:

1
2
Quartz does not store an actual instance of a <code>Job</code> class, but
* instead allows you to define an instance of one, through the use of a <code>JobDetail</code>.

为什么这么做呢?Job本身仅会有业务逻辑,不存其它信息。如果在Job运行时,我们想给Job传入某些变量值,可以借助JobDetail。

JobDetail内部有JobDataMap,调度器实例化一个Job,调用Job的execute方法前,可以往JobDataMap中塞值。这样一来,Job在execute的时候,可以从JobExecutionContext里拿到JobDataMap,进而拿到特定的变量值。

Trigger

利用好触发器,你可以让Job在你希望的时间点执行。每个Job可以拥有多个触发器,一个触发器只能绑定到一个Job。

调度器在调度的时候,会扫描所有的触发器,分别执行它们。针对每个Job,Quartz会用线程池里的线程执行,如果多个触发器都在同一时间点触发,而Quartz内部没有足够多的线程跑触发器对应的Job,那么会随机或是优先选择某些Job。

如果你希望你的Job间隔特定的时间执行,可以用SimpleTrigger;如果你的Job没有固定的间隔时间,可以用CronTrigger,配置上cron表达式。

触发失败

在触发失败时,Quartz有多种补偿策略,比如放弃触发,重新触发等。

调度器

调度器有2种线程,一种用来执行Job,一种用来轮询扫描所有触发器,若符合条件则进行触发。

Listener

Quartz为了加强定制性,给触发器、Job、调度器都添加了监听器的支持。

触发器监听器

当触发器发生以下事件时,会被触发器监听器TriggerListener识别到:被触发、触发失败、触发完成(下一步将是Job的运行)
你可以自己实现TriggerListener接口,在相应事件发生时执行某些操作,比如记日志,比如发邮件等。

Job监听器

当Job即将开始执行(此时触发器已经触发完成)、或是执行完毕时,相应事件可以被JobListener识别到。

调度器监听器

SchedulerListener识别的事件有:添加/移除用户的Job/触发器监听器、发生错误、调度器被shutdown。

特性

JobStores

这个东西不对Quartz的使用者开放,仅为Quartz幕后使用,它的作用是存储Quartz运行过程中所有的数据。

RamJobStore把数据存储在内存中;JDBCJobStore把数据存储在数据库里,支持多种数据库(涉及到数据库,就会有事务,Quartz自身有事务管理器,当然,你也可以让别的框架来管理事务,比如spring)。

线程池

Quartz内部是有线程池的,用来运行Job,一般情况下,5个线程就够了。

集群

Quartz支持集群配置,设置“org.quartz.jobStore.isClustered”参数为true可以开启,不过它对集群内机器的时间同步有比较高的要求,误差不超过1秒。

Quartz的集群没有中心管理节点,机器与机器之间根据数据库里的数据来判断其它机器的存在。

每个机器上的调度器定时到数据库中签到,刷新自己的存活时间。如果某个调度器很久不签到,其它调度器就会默认该调度器已经挂掉,接过它手中的Job继续运行(如果Job配置了恢复属性的话,如果Job没配置该属性,那么只能等待下一次的触发了)。