简介

What is the Quartz Job Scheduling Library?
Quartz is a richly featured, open source job scheduling library that can be integrated within virtually any Java application - from the smallest stand-alone application to the largest e-commerce system. Quartz can be used to create simple or complex schedules for executing tens, hundreds, or even tens-of-thousands of jobs; jobs whose tasks are defined as standard Java components that may execute virtually anything you may program them to do. The Quartz Scheduler includes many enterprise-class features, such as support for JTA transactions and clustering.

Quartz is freely usable, licensed under the Apache 2.0 license.

Quartz Job Scheduling Library是什么? Quartz是功能强大的开源作业调度库,几乎可以集成到任何Java应用程序中-从最小的独立应用程序到最大的电子商务系统。 Quartz可用于创建简单或复杂的计划,以执行数以十计,百计,万计的工作。任务标准Java组件的任务,都可以执行您对其执行的任何编程操作。Quartz Scheduler包含许多企业级功能,例如对JTA事务和集群的支持。

Quartz是免费使用的,并根据Apache 2.0许可获得许可。

Quartz 是 OpenSymphony 开源组织在任务调度领域的一个开源项目,完全基于 Java 实现。该项目于 2009 年被 Terracotta 收购,目前是 Terracotta 旗下的一个项目。官网地址:http://www.quartz-scheduler.org/Quartz

用一个小Java库发布文件(.jar文件),这个库文件包含了所有Quartz核心功能。这些功能的主要接口(API)是Scheduler接口。它提供了简单的操作,例如:将任务纳入日程或者从日程中取消,开始/停止/暂停日程进度。

功能

  • 持久性任务 - 就是保持调度定时的状态;
  • 任务管理 - 对调度任务进行有效的管理,“任务进度管理器”就是一个在预先确定(被纳入日程)的时间到达时,负责执行(或者通知)其他软件组件的系统。;

例如:

  • 自动关闭30分钟未支付的订单
  • 与第三方公司对账业务
  • 数据统计,比如博客系统统计日粉丝数,日阅读量等
  • 活动开始和结束通知;
  • 想在每月25号,自动还款;
  • 每周或者每月的提醒事项,比如周总结或者月总结;

像这种某个时间点执行任务,或者每隔一段时间重复执行任务,都可以用Quartz实现

特点

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

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

    类型 优点 缺点
    RAMJobStore 不要外部数据库,配置容易,运行速度快 因为调度程序信息是存储在被分配给JVM的内存里面,所以,当应用程序停止运行时,所有调度信息将被丢失。另外因为存储到JVM内存里面,所以可以存储多少个Job和Trigger将会受到限制
    JDBCJobStore 支持集群,因为所有的任务信息都会保存到数据库中,可以控制事物,还有就是如果应用服务器关闭或者重启,任务信息都不会丢失,并且可以恢复因服务器关闭或者重启而导致执行失败的任务 运行速度的快慢取决与连接数据库的快慢
  • 支持分布式集群,具有伸缩性和高可用性,支持负载均衡。

  • 完全由Java写成,方便集成(Spring)

当然除开Quartz我们也可以使用这些工具来完成任务的管理:

  • java.util.Timer:一个 JDK 中自带的处理简单定时任务的工具
  • java.util.concurrent.ScheduledExecutorService:JDK 中的定时任务接口,可以将定时任务与线程池功能结合使用
  • org.springframework.scheduling.annotation.Scheduled:Spring 框架中基于注解来实现定时任务处理。

核心组件

Quartz框架内部主要类的关系如下:

下面我们主要介绍Scheduler、Job 、JobDetail、Trigger四个类

Job类

首先我们需要定义实现一个定时功能的接口,我们可以称之为Task(或Job),如定时发送邮件的task(Job),重启机器的task(Job),优惠券到期发送短信提醒的task(Job)。

即表示要执行的具体工作或者被调度的任务。我们的任务类实现该接口,重写execute方法来定义任务的执行逻辑。主要有两种类型的job:无状态的(stateless)和有状态的(stateful)。对于同一个trigger来说,有状态的job不能被并行执行,只有上一次触发的任务被执行完之后,才能触发下一次执行。Job主要有两种属性:volatility和durability,其中volatility表示任务是否被持久化到数据库存储,而durability表示在没有trigger关联的时候任务是否被保留。两者都是在值为true的时候任务被持久化或保留。一个job可以被多个trigger关联,但是一个trigger只能关联一个job。

例如:

1
2
3
4
5
6
7
8
9
10
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

public class AlphaJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
System.out.println(Thread.currentThread().getName() + ": execute a quartz job.");
}
}

JobDetail类

JobDetail定义任务详情。包含执行任务的Job,任务的一些身份信息(可以帮助找到这个任务),给任务设置JobDataMap(把参数带到任务里面去)。JobDetail里面常用方法如下:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public interface JobDetail {

/**
* job的身份,通过他来找到对应的job
*/
public JobKey getKey();

/**
* job描述
*/
public String getDescription();

/**
* 执行job的具体类,定时任务的动作都在这个类里面完成
*/
public Class<? extends Job> getJobClass();

/**
* 给job传递数据(把需要的参数带到job执行的类里面去)
*/
public JobDataMap getJobDataMap();

/**
* 任务孤立的时候是否需要继续报错(孤立:没有触发器关联该任务)
*/
public boolean isDurable();

/**
* 和@PersistJobDataAfterExecution注解一样
* PersistJobDataAfterExecution注解是添加在Job类上的:表示 Quartz 将会在成功执行 execute()
* 方法后(没有抛出异常)更新 JobDetail 的 JobDataMap,下一次执行相同的任务(JobDetail)
* 将会得到更新后的值,而不是原始的值
*/
public boolean isPersistJobDataAfterExecution();

/**
* 和DisallowConcurrentExecution注解的功能一样
* DisallowConcurrentExecution注解添加到Job之后,Quartz 将不会同时执行多个 Job 实例,
* 怕有数据更新的时候不知道取哪一个数据
*/
public boolean isConcurrentExectionDisallowed();

/**
* 指示调度程序在遇到“恢复”或“故障转移”情况时是否应重新执行作业
*/
public boolean requestsRecovery();


/**
* JobDetail是通过构建者模式来实现的
*/
public JobBuilder getJobBuilder();
}

JobDetail实例一般是通过JobBuilder来创建(Build模式)。例如,我们定义一个简单的JobDetail,Job执行逻辑类是AlphaJob类,Job名字是AlphaJob,Job组是groupName**(job名字和job组是job的唯一标识)**。同时我们还给job传递了一个字符串参数AlphaJob。

1
2
3
4
5
6
7
JobDetail jobDetail = JobBuilder
.newJob(AlphaJob.class)
.withIdentity("Alpha", "groupName")
.build();
// 参数使用
JobDataMap map = jobDetail.getJobDataMap();
map.put("JOB_NAME", "AlphaJob");

Trigger类

Trigger触发器,设置Job什么时候执行。

触发器 试用场景
SimpleTrigger 简单触发器,适用于 按指定的时间间隔执行多少次任务的情况
CronTrigger Cron触发器,通过Cron表达式来控制任务的执行时间
DailyTimeIntervalTrigger 日期触发器,在给定的时间范围内或指定的星期内以秒、分钟或者小时为周期进行重复的情况
CalendarIntervalTrigger 日历触发器,根据一个给定的日历时间进行重复

PS: 一个Trigger只能绑定一个Job。但是一个Job可以被多个Trigger绑定。

最常用的有:

SimpleTrigger

用来触发只需执行一次或者在给定时间触发并且重复N次且每次执行延迟一定时间的任务。

SimpleTrigger由SimpleScheduleBuilder构建生成:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
/**
* 每分钟都执行,执行无限次
*/
public static SimpleScheduleBuilder repeatMinutelyForever();

/**
* 每隔minutes分钟执行一次,循环无限次
*/
public static SimpleScheduleBuilder repeatMinutelyForever(int minutes);

/**
* 每秒执行一次,循循环无限次
*/
public static SimpleScheduleBuilder repeatSecondlyForever();

/**
* 每隔seconds秒执行一次,循环无限次
*/
public static SimpleScheduleBuilder repeatSecondlyForever(int seconds);

/**
* 每小时执行一次,循环无限次
*/
public static SimpleScheduleBuilder repeatHourlyForever();

/**
* 每hours小时执行一次,循环无限次
*/
public static SimpleScheduleBuilder repeatHourlyForever(int hours);

/**
* 每分钟执行一次,执行count次
*/
public static SimpleScheduleBuilder repeatMinutelyForTotalCount(int count);

/**
* 每minutes执行一次,执行count次
*/
public static SimpleScheduleBuilder repeatMinutelyForTotalCount(int count, int minutes);

/**
* 每秒执行一次,执行count次
*/
public static SimpleScheduleBuilder repeatSecondlyForTotalCount(int count);

/**
* 每seconds执行一次,执行count次
*/
public static SimpleScheduleBuilder repeatSecondlyForTotalCount(int count, int seconds);

/**
* 每小时执行一次,执行count次
*/
public static SimpleScheduleBuilder repeatHourlyForTotalCount(int count);

/**
* 每hours执行一次,执行count次
*/
public static SimpleScheduleBuilder repeatHourlyForTotalCount(int count, int hours) ;


/**
* 任务执行的时间间隔 -- 单位毫秒
*/
public SimpleScheduleBuilder withIntervalInMilliseconds(long intervalInMillis);

/**
* 任务执行的时间间隔 -- 单位秒
*/
public SimpleScheduleBuilder withIntervalInSeconds(int intervalInSeconds);

/**
* 任务执行的时间间隔 -- 单位分
*/
public SimpleScheduleBuilder withIntervalInMinutes(int intervalInMinutes);

/**
* 任务执行的时间间隔 -- 单位小时
*/
public SimpleScheduleBuilder withIntervalInHours(int intervalInHours);

/**
* S任务执行次数
*/
public SimpleScheduleBuilder withRepeatCount(int triggerRepeatCount);

/**
* 任务执行无限次
*/
public SimpleScheduleBuilder repeatForever();

/**
* 这个不是忽略已经错失的触发的意思,而是说忽略MisFire策略。它会在资源合适的时候,重新触发所有的MisFire任务,并且不会影响现有的调度时间。
*
* 比如,SimpleTrigger每15秒执行一次,而中间有5分钟时间它都MisFire了,一共错失了20个,5分钟后,假设资源充足了,并且任务允许并发,它会被一次性触发
*/
public SimpleScheduleBuilder withMisfireHandlingInstructionIgnoreMisfires() {
misfireInstruction = Trigger.MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY;
return this;
}

/**
* 忽略已经MisFire的任务,并且立即执行调度。这通常只适用于只执行一次的任务
*/

public SimpleScheduleBuilder withMisfireHandlingInstructionFireNow() {
misfireInstruction = SimpleTrigger.MISFIRE_INSTRUCTION_FIRE_NOW;
return this;
}

/**
* 在下一次调度时间点,重新开始调度任务,包括MisFire的
*/
public SimpleScheduleBuilder withMisfireHandlingInstructionNextWithExistingCount() {
misfireInstruction = SimpleTrigger.MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT;
return this;
}

/**
* 在下一次调度时间点,重新开始调度任务,忽略已经MisFire的任务
*/
public SimpleScheduleBuilder withMisfireHandlingInstructionNextWithRemainingCount() {
misfireInstruction = SimpleTrigger.MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT;
return this;
}

/**
* 将startTime设置当前时间,立即重新调度任务,包括MisFire的
*/
public SimpleScheduleBuilder withMisfireHandlingInstructionNowWithExistingCount() {
misfireInstruction = SimpleTrigger.MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT;
return this;
}

/**
* 将startTime设置当前时间,立即重新调度任务,会忽略已经MisFire的任务
*/
public SimpleScheduleBuilder withMisfireHandlingInstructionNowWithRemainingCount() {
misfireInstruction = SimpleTrigger.MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT;
return this;
}

例如,定义一个触发器,每隔30S执行一次,总共执行100次:

1
2
3
4
5
6
7
// 使用simpleTrigger规则, 任务每隔30S执行一次,执行100次
SimpleTrigger trigger = TriggerBuilder
.newTrigger()
.withIdentity(jobName, jobGroupName)
.withSchedule(SimpleScheduleBuilder.repeatSecondlyForTotalCount(30, 100))
.startNow()
.build();

CronTrigger

按照日历触发,例如“每个周五”,每个月10日中午或者10:15分。

CronTrigger使用Cron表达是来定义任务的触发时间。相对来说比较灵活,对于复杂的业务需求来说更加的实用。CronTrigger由CronScheduleBuilder构建而成:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
/**
* 创建CronScheduleBuilder对象,设置Cron表达式 如果Cron表达式解析异常抛RuntimeException
*/
public static CronScheduleBuilder cronSchedule(String cronExpression);

/**
* 创建CronScheduleBuilder对象,设置Cron表达式 如果Cron表达式解析异常需要自己处理
*/
public static CronScheduleBuilder cronScheduleNonvalidatedExpression(
String cronExpression) throws ParseException;

/**
* 创建CronScheduleBuilder对象,参数是CronExpression
*/
public static CronScheduleBuilder cronSchedule(CronExpression cronExpression);

/**
* 每天的hour时,minute分执行任务
*/
public static CronScheduleBuilder dailyAtHourAndMinute(int hour, int minute);

/**
* 每个礼拜的哪几天(daysOfWeek)的hour时minute分执行任务
*/

public static CronScheduleBuilder atHourAndMinuteOnGivenDaysOfWeek(
int hour, int minute, Integer... daysOfWeek);

/**
* 每个礼拜的dayOfWeek,的hour时minute分执行任务
*/
public static CronScheduleBuilder weeklyOnDayAndHourAndMinute(
int dayOfWeek, int hour, int minute);

/**
* 每个月的那天(dayOfMonth)的hour时minure分执行任务
*/
public static CronScheduleBuilder monthlyOnDayAndHourAndMinute(
int dayOfMonth, int hour, int minute);

/**
* 设置时区
*/
public CronScheduleBuilder inTimeZone(TimeZone timezone);

/**
* 这个不是忽略已经错失的触发的意思,而是说忽略MisFire策略。它会在资源合适的时候,重新触发所有的MisFire任务,并且不会影响现有的调度时间。
*/
public CronScheduleBuilder withMisfireHandlingInstructionIgnoreMisfires() {
misfireInstruction = Trigger.MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY;
return this;
}

/**
* 不对MisFire的任务做任何处理,错过了就是错过了
*/
public CronScheduleBuilder withMisfireHandlingInstructionDoNothing() {
misfireInstruction = CronTrigger.MISFIRE_INSTRUCTION_DO_NOTHING;
return this;
}

/**
* 针对MisFire的任务马上执行一次
*/
public CronScheduleBuilder withMisfireHandlingInstructionFireAndProceed() {
misfireInstruction = CronTrigger.MISFIRE_INSTRUCTION_FIRE_ONCE_NOW;
return this;
}

Cron表达式规则如下:

1
[秒] [分] [时] [日] [月] [周] [年]

通常定义 “年” 的部分可以省略,实际常用的由 前六部分组成

关于 cron 的各个域的定义如下表格所示:

是否必填 值以及范围 通配符
0-59 , - * /
0-59 , - * /
0-23 , - * /
1-31 , - * ? / L W
1-12 或 JAN-DEC , - * /
1-7 或 SUN-SAT , - * ? / L #
1970-2099 , - * /
  • , 这里指的是在两个以上的时间点中都执行,如果我们在 “分” 这个域中定义为 8,12,35 ,则表示分别在第8分,第12分 第35分执行该定时任务。
  • - 这个比较好理解就是指定在某个域的连续范围,如果我们在 “时” 这个域中定义 1-6,则表示在1到6点之间每小时都触发一次,用 , 表示 1,2,3,4,5,6
  • * 表示所有值,可解读为 “每”。 如果在“日”这个域中设置 *,表示每一天都会触发。
  • ? 表示不指定值。使用的场景为不需要关心当前设置这个字段的值。例如:要在每月的8号触发一个操作,但不关心是周几,我们可以这么设置 0 0 0 8 * ?
  • / 在某个域上周期性触发,该符号将其所在域中的表达式分为两个部分,其中第一部分是起始值,除了秒以外都会降低一个单位,比如 在 “秒” 上定义 5/10 表示从 第 5 秒开始 每 10 秒执行一次,而在 “分” 上则表示从 第 5 秒开始 每 10 分钟执行一次。
  • L 表示英文中的LAST 的意思,只能在 “日”和“周”中使用。在“日”中设置,表示当月的最后一天(依据当前月份,如果是二月还会依据是否是润年), 在“周”上表示周六,相当于”7”或”SAT”。如果在”L”前加上数字,则表示该数据的最后一个。例如在“周”上设置”7L”这样的格式,则表示“本月最后一个周六”
  • W 表示离指定日期的最近那个工作日(周一至周五)触发,只能在 “日” 中使用且只能用在具体的数字之后。若在“日”上置”15W”,表示离每月15号最近的那个工作日触发。假如15号正好是周六,则找最近的周五(14号)触发, 如果15号是周未,则找最近的下周一(16号)触发.如果15号正好在工作日(周一至周五),则就在该天触发。如果是 “1W” 就只能往本月的下一个最近的工作日推不能跨月往上一个月推。
  • # 表示每月的第几个周几,只能作用于 “周” 上。例如 ”2#3” 表示在每月的第三个周二。

举例如下:

使用CronTrigger例子如下:

  • 定义一个每天15:30执行的一个任务。代码如下:
1
2
3
4
5
String cronExpression = String.format("0 %d %d ? * *", 15, 30);
CronTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity(jobName, jobGroupName)// 触发器名,触发器组
.withSchedule(CronScheduleBuilder.cronSchedule(cronExpression)) // CronScheduleBuilder
.build();
  • 定义一个每个礼拜星期六,15:30执行的任务,代码如下:
1
2
3
4
5
CronTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity(jobName, jobGroupName)// 触发器名,触发器组
//每个礼拜星期六的15"30执行
.withSchedule(CronScheduleBuilder.weeklyOnDayAndHourAndMinute(DateBuilder.SATURDAY, 15, 30))
.build();

Scheduler类

任务调度器,一个调度容器中可以注册多个JobDetail和Trigger。当Trigger与JobDetail组合,就可以被Scheduler容器调度了。正常情况下一个应用只需要一个Scheduler对象。

Scheduler 由 SchedulerFactory 创建:DirectSchedulerFactory或者StdSchedulerFactory。第二种工厂StdSchedulerFactory使用较多,因为DirectSchedulerFactory使用起来不够方便,需要作许多详细的手工编码设置。Scheduler主要有三种:RemoteMBeanScheduler,RemoteScheduler和StdScheduler。

Scheduler调度器,是Quartz框架的心脏,用来管理Trigger和Job,并保证Job能在Trigger设置的时间被触发执行。一般情况下调度器启动之后,我们不需要做任何处理。

Scheduler主要函数介绍如下:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
/**
* 方法获取的是正在执行的Job
*/
List<JobExecutionContext> getCurrentlyExecutingJobs() throws SchedulerException;



/**
* 获取Scheduler上的监听器ListenerManager, 比如可以监听job,trigger添加移除的状态等
*/
ListenerManager getListenerManager() throws SchedulerException;

/**
* 把jobDetail添加到调度系统中,并且把任务和Trigger关联起来
*/
Date scheduleJob(JobDetail jobDetail, Trigger trigger)
throws SchedulerException;

/**
* 开始调度Trigger关联的job
*/
Date scheduleJob(Trigger trigger) throws SchedulerException;

/**
* 开始调度job,同时设置多个
*/
void scheduleJobs(Map<JobDetail, Set<? extends Trigger>> triggersAndJobs, boolean replace) throws SchedulerException;

/**
* 开始调度job,而且这个job可以关联一个或多个触发器Trigger
*/
void scheduleJob(JobDetail jobDetail, Set<? extends Trigger> triggersForJob, boolean replace) throws SchedulerException;

/**
* 从触发器中移除Trigger(Trigger对应的任务会被移除掉)
*/
boolean unscheduleJob(TriggerKey triggerKey)
throws SchedulerException;

/**
* 从触发器中移除Trigger(Trigger对应的任务会被移除掉)
*/
boolean unscheduleJobs(List<TriggerKey> triggerKeys)
throws SchedulerException;

/**
* 移除triggerKey,添加newTrigger
*/
Date rescheduleJob(TriggerKey triggerKey, Trigger newTrigger)
throws SchedulerException;

/**
* 添加job到触发器中,当然这个时候任务是不会执行的,触发关联到了触发器Trigger上
*/
void addJob(JobDetail jobDetail, boolean replace)
throws SchedulerException;

/**
* 添加任务
*/
void addJob(JobDetail jobDetail, boolean replace, boolean storeNonDurableWhileAwaitingScheduling)
throws SchedulerException;

/**
* 删除任务
*/
boolean deleteJob(JobKey jobKey)
throws SchedulerException;

/**
* 删除任务
*/
boolean deleteJobs(List<JobKey> jobKeys)
throws SchedulerException;

/**
* 立即执行任务
*/
void triggerJob(JobKey jobKey)
throws SchedulerException;

/**
* 立即执行任务
*/
void triggerJob(JobKey jobKey, JobDataMap data)
throws SchedulerException;

/**
* 暂停任务
*/
void pauseJob(JobKey jobKey)
throws SchedulerException;

/**
* 暂停任务
*/
void pauseJobs(GroupMatcher<JobKey> matcher) throws SchedulerException;

/**
* 暂停触发器对应的任务
*/
void pauseTrigger(TriggerKey triggerKey)
throws SchedulerException;

/**
* 暂停触发器对应的任务
*/
void pauseTriggers(GroupMatcher<TriggerKey> matcher) throws SchedulerException;

/**
* 恢复任务
*/
void resumeJob(JobKey jobKey)
throws SchedulerException;

/**
* 恢复任务
*/
void resumeJobs(GroupMatcher<JobKey> matcher) throws SchedulerException;

/**
* 恢复触发器对应的任务
*/
void resumeTrigger(TriggerKey triggerKey)
throws SchedulerException;

/**
* 恢复触发器对应的任务
*/
void resumeTriggers(GroupMatcher<TriggerKey> matcher) throws SchedulerException;

/**
* 暂停所有的任务
*/
void pauseAll() throws SchedulerException;

/**
* 恢复所有的任务
*/
void resumeAll() throws SchedulerException;

/**
* 获取所有任务的jobGroup名字
*/
List<String> getJobGroupNames() throws SchedulerException;

/**
* 获取jobKey
*/
Set<JobKey> getJobKeys(GroupMatcher<JobKey> matcher) throws SchedulerException;

/**
* 获取任务对应的触发器Trigger
*
*/
List<? extends Trigger> getTriggersOfJob(JobKey jobKey)
throws SchedulerException;

/**
* 获取所有触发器的Group name
*/
List<String> getTriggerGroupNames() throws SchedulerException;

/**
* 获取TriggerKey
*/
Set<TriggerKey> getTriggerKeys(GroupMatcher<TriggerKey> matcher) throws SchedulerException;

/**
* 获取所有暂停任务对应的触发器的Group Name
*/
Set<String> getPausedTriggerGroups() throws SchedulerException;

/**
* 获取JobDetail
*
*/
JobDetail getJobDetail(JobKey jobKey)
throws SchedulerException;

/**
* 获取触发器Trigger
*/
Trigger getTrigger(TriggerKey triggerKey)
throws SchedulerException;

/**
* 获取触发器的状态
*/
Trigger.TriggerState getTriggerState(TriggerKey triggerKey)
throws SchedulerException;

/**
* 恢复触发器的状态
*/
void resetTriggerFromErrorState(TriggerKey triggerKey)
throws SchedulerException;
/**
* 添加Calendar
* 这里稍微解释下Calendar:Quartz的Calendar可以用于排除一些特定的日期不执行任务
*/
void addCalendar(String calName, Calendar calendar, boolean replace, boolean updateTriggers)
throws SchedulerException;

/**
* 删除Calendar
*/
boolean deleteCalendar(String calName) throws SchedulerException;

/**
* 获取Calendar
*/
Calendar getCalendar(String calName) throws SchedulerException;

/**
* 获取Calendar对应的名字
*/
List<String> getCalendarNames() throws SchedulerException;

/**
* 中断某个任务
*/
boolean interrupt(JobKey jobKey) throws UnableToInterruptJobException;

/**
* 中断任务
* JobExecutionContext#getFireInstanceId()
*/
boolean interrupt(String fireInstanceId) throws UnableToInterruptJobException;

/**
* 判断对应job是否存在
*/
boolean checkExists(JobKey jobKey) throws SchedulerException;

/**
* 判断对应触发器是否存在
*/
boolean checkExists(TriggerKey triggerKey) throws SchedulerException;

Quartz 集群

Quartz的集群部署方案在架构上是分布式的,没有负责集中管理的节点,而是利用数据库锁的方式来实现集群环境下进行并发控制。通过基于数据库对分布式锁来达到并发控制对目的。

数据库核心表如下:

Table Name Description
QRTZ_CALENDARS 存储Quartz的Calendar信息
QRTZ_CRON_TRIGGERS 存储CronTrigger,包括Cron表达式和时区信息
QRTZ_FIRED_TRIGGERS 存储与已触发的Trigger相关的状态信息,以及相联Job的执行信息
QRTZ_PAUSED_TRIGGER_GRPS 存储已暂停的Trigger组的信息
QRTZ_SCHEDULER_STATE 存储少量的有关Scheduler的状态信息,和别的Scheduler实例
QRTZ_LOCKS 存储程序的悲观锁的信息
QRTZ_JOB_DETAILS 存储每一个已配置的Job的详细信息
QRTZ_JOB_LISTENERS 存储有关已配置的JobListener的信息
QRTZ_SIMPLE_TRIGGERS 存储简单的Trigger,包括重复次数、间隔、以及已触的次数
QRTZ_BLOG_TRIGGERS Trigger作为Blob类型存储
QRTZ_TRIGGER_LISTENERS 存储已配置的TriggerListener的信息
QRTZ_TRIGGERS 存储已配置的Trigger的信息

其中,QRTZ_LOCKS就是Quartz集群实现同步机制的行锁表,其表结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
--QRTZ_LOCKS表结构
CREATE TABLE `QRTZ_LOCKS` (
`LOCK_NAME` varchar(40) NOT NULL,
PRIMARY KEY (`LOCK_NAME`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

--QRTZ_LOCKS记录
+-----------------+
| LOCK_NAME |
+-----------------+
| CALENDAR_ACCESS |
| JOB_ACCESS |
| MISFIRE_ACCESS |
| STATE_ACCESS |
| TRIGGER_ACCESS |
+-----------------+

可以看出QRTZ_LOCKS中有5条记录,代表5把锁,分别用于实现多个Quartz Node对Job、Trigger、Calendar访问的同步控制。

QuartzScheduler调度线程不断获取trigger,触发trigger,释放trigger,流程如下:

每当要进行与某种业务相关的数据库操作时,先去QRTZ_LOCKS表中查询操作相关的业务对象所需要的锁,在select语句之后加for update来实现。例如,TRIGGER_ACCESS表示对任务触发器相关的信息进行修改、删除操作时所需要获得的锁。这时,执行查询这个表数据的SQL形如:

1
select * from QRTZ_LOCKS t where t.lock_name='TRIGGER_ACCESS' for update

当一个线程使用上述的SQL对表中的数据执行查询操作时,若查询结果中包含相关的行,数据库就对该行进行ROW LOCK;若此时,另外一个线程使用相同的SQL对表的数据进行查询,由于查询出的数据行已经被数据库锁住了,此时这个线程就只能等待,直到拥有该行锁的线程完成了相关的业务操作,执行了commit动作后,数据库才会释放了相关行的锁,这个线程才能继续执行。

通过这样的机制,在集群环境下,结合悲观锁的机制就可以防止一个线程对数据库数据的操作的结果被另外一个线程所覆盖,从而可以避免一些难以觉察的错误发生。当然,达到这种效果的前提是需要把Connection设置为手动提交,即autoCommit为false。

在Spring Boot中使用Quartz

在Spring Boot中,相关类的配置被大大的简化,主要有以下几部分操作:

导入依赖

我们在项目的pom.xml文件中添加Quartz的相关依赖

1
2
3
4
5
<!-- quartz 定时任务 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

配置Quartz

我们在application.properties 中对Quartz进行相关配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
# QuartzProperties
# 将任务等保存到数据库中
spring.quartz.job-store-type=jdbc
# scheduler的实例名
spring.quartz.scheduler-name=communityScheduler
spring.quartz.properties.org.quartz.scheduler.instanceId=AUTO
# 持久化相关
spring.quartz.properties.org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
spring.quartz.properties.org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
spring.quartz.properties.org.quartz.jobStore.isClustered=true
spring.quartz.properties.org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
# 线程池线程数
spring.quartz.properties.org.quartz.threadPool.threadCount=5

创建Job类实例

代码类似同上:

1
2
3
4
5
6
7
8
9
10
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

public class AlphaJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
System.out.println(Thread.currentThread().getName() + ": execute a quartz job.");
}
}

创建Configuration类

直接在代码里注册JobDetail和Trigger的bean就可以了

只要已启动服务,配置文件默认会被加载,Quartz根据相关配置对数据库进行写入,同时调度器Scheduler也会根据数据进行调度。

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.JobDetailFactoryBean;
import org.springframework.scheduling.quartz.SimpleTriggerFactoryBean;

// 配置 -> 数据库 -> 调用
@Configuration
public class QuartzConfig {

// FactoryBean可简化Bean的实例化过程:
// 1.通过FactoryBean封装Bean的实例化过程.
// 2.将FactoryBean装配到Spring容器里.
// 3.将FactoryBean注入给其他的Bean.
// 4.该Bean得到的是FactoryBean所管理的对象实例.

// 配置JobDetail
@Bean
public JobDetailFactoryBean alphaJobDetail() {
JobDetailFactoryBean factoryBean = new JobDetailFactoryBean();
factoryBean.setJobClass(AlphaJob.class);
factoryBean.setName("alphaJob");
factoryBean.setGroup("alphaJobGroup");
factoryBean.setDurability(true);
factoryBean.setRequestsRecovery(true);
return factoryBean;
}

// 配置Trigger(SimpleTriggerFactoryBean, CronTriggerFactoryBean)
@Bean
public SimpleTriggerFactoryBean alphaTrigger(JobDetail alphaJobDetail) {
SimpleTriggerFactoryBean factoryBean = new SimpleTriggerFactoryBean();
factoryBean.setJobDetail(alphaJobDetail);
factoryBean.setName("alphaTrigger");
factoryBean.setGroup("alphaTriggerGroup");
factoryBean.setRepeatInterval(3000);
factoryBean.setJobDataMap(new JobDataMap());
return factoryBean;
}

// 刷新帖子分数任务
@Bean
public JobDetailFactoryBean postScoreRefreshJobDetail() {
JobDetailFactoryBean factoryBean = new JobDetailFactoryBean();
factoryBean.setJobClass(PostScoreRefreshJob.class);
factoryBean.setName("postScoreRefreshJob");
factoryBean.setGroup("communityJobGroup");
factoryBean.setDurability(true);
factoryBean.setRequestsRecovery(true);
return factoryBean;
}

@Bean
public SimpleTriggerFactoryBean postScoreRefreshTrigger(JobDetail postScoreRefreshJobDetail) {
SimpleTriggerFactoryBean factoryBean = new SimpleTriggerFactoryBean();
factoryBean.setJobDetail(postScoreRefreshJobDetail);
factoryBean.setName("postScoreRefreshTrigger");
factoryBean.setGroup("communityTriggerGroup");
factoryBean.setRepeatInterval(1000 * 60 * 5);
factoryBean.setJobDataMap(new JobDataMap());
return factoryBean;
}
}