简介
Quartz,由Java编写、用于Java程序执行定时任务。可JVM独立运行,也可集成使用。
- 可以通过 Calendar 执行(排除节假日)
- 指定某个时间无限循环执行, 比如每五分钟执行一次
- 固定时间执行,如每周周一上午10点执行,每月2号上午10点执行
相较于java.util.Timer
, Quartz增加了很多功能:
- 持久性作业 - 保持调度定时的状态;
- 作业管理 - 对调度作业进行有效的管理;
Quartz核心概念 | 中文 | 描述 |
---|---|---|
Job | 任务 | 具体的作业。what to do |
Trigger | 触发器 | 触发Job,指定Job的执行时间、执行间隔、运行次数等。 when to do |
Scheduler | 调度器 | 联接Job和Trigger,指定Trigger执行指定Job |
一个 Job 可以对应多个 Trigger, 但一个 Trigger 只能对应一个 Job。job(1) <=> Trigger(n)
graph TB A[Scheduler] A ==> B[Job] A ==> C[Trigger] B -.- B1(JobDetail) B -.- B2(JobExecutionContext) C -.- C1(CronTrigger) C -.- C2(SimpleTrigger)
配置核心概念的方式:
- java直接配置并实现。
- xml配置Quartz参数,Java只负责业务实现。
- 和spring相关框架搭配。
Job
定义需要具体执行的任务。
Job
是个接口,只有一个方法execute(JobExecutionContext context)
,在实现类的execute中编写业务逻辑。
- JobExecutionContext:包含了Quartz运行时的环境以及Job本身的详细数据信息,其中Job运行时的信息保存在 JobDataMap 实例中。
1 | // Job接口源码 |
JobDetail
用来绑定指定的Job,为Job实例提供属性:
- name:Job 名字
- group
- jobClass
- jobDataMap:实现了JDK的Map接口,可以以Key-Value的形式存储数据。
JobDetail
、Trigger
都可以使用JobDataMap来设置一些参数信息 - 不直接接受一个 Job 的实例,相反它接收一个 Job 实现类,以便运行时通过
newInstance()
的反射机制实例化 Job。
每次Scheduler调度执行一个Job的时候,①先根据JobDetail找到对应的Job,②根据JobDetail重新创建一个Job实例
,③执行Job实例
的execute方法。④任务执行结束后,关联的Job实例
会被释放,且被JVM GC清除。
JobDetail定义的是任务配置,真正执行是Job。
这是因为任务有可能【并发执行】,如果Scheduler直接使用Job,会存在对同一Job实例并发访问的问题。
而JobDetail & Job 方式,Scheduler每次执行,都会根据JobDetail创建一个新的Job实例,这样就可以规避并发访问的问题。
1 | // 绑定AutoPrint这个执行逻辑 |
任务状态
有状态Job:
- 共享同一个 JobDataMap 实例,每次任务执行对 JobDataMap 所做的更改会保存下来,后面的执行可以看到这个更改,即每次执行任务后都会对后面的执行发生影响。
- 不能并发执行。上次job未执行完毕,则下次job将
阻塞等待
,直到上次job执行完毕。
无状态Job:
- 在执行时拥有自己的 JobDataMap 拷贝,对 JobDataMap 的更改不会影响下次的执行
- 能并发执行。
监听
JobListener 监听任务执行前事件、任务执行后事件;
Trigger
触发任务。是个接口,描述触发 job 执行的时间触发规则。
- new Trigger().startAt():表示触发器首次被触发的时间;
- new Trigger().endAt():表示触发器结束触发的时间;
包括两类触发器:
- SimpleTrigger:简单,适合次数和间隔
- CronTrigger:灵活常用,适合次数、间隔等,也适合精准定时。
也可以使用ScheduleBuilder的子类来触发操作:
- SimpleScheduleBuilder
- CronScheduleBuilder。
SimpleTrigger
- 强调次数,比如每天执行若干次。用于当且仅当需调度一次、或者以固定时间间隔周期执行调度。
- 精准指定间隔,如,程序运行5s后开始执行Job,执行Job 5s后结束执行。
- 无法设置具体时间,及不适合每天定时执行任务的场景,比如不能设置每天半夜十二点执行任务。
1 | //创建SimpleTrigger startTime end 分别表示触发器的首次触发时间和不再被触发的时间 |
CronTrigger
基于Cron表达式定义更灵活的时间规则,是基于日历的作业调度。
1 | //创建CornTrigger 比如下面的就是设计每天中午十二点整触发 |
Cron表达式
设置定时任务执行的时间
1 | <bean class="org.springframework.scheduling.quartz.CronTriggerFactoryBean"> |
cronExpression是以5或6个空格隔开的字符串。分成6或7个域。每个域代表一个含义。
在线生成Cron表达式:https://cron.qqe2.com/
1 | # 7种 |
字符不区分大小写
含义 | 可能值 | 有效范围 |
---|---|---|
Seconds | , - * / | 0-59的整数 |
Minutes | , - * / | 0-59的整数 |
Hours | , - * / | 0-23的整数 |
DayofMonth | , - * / ? L W C | 0-31的整数 |
Month | , - * / | 0-11的整数或JAN-DEc |
DayofWeek | , - * / ? L C # | 1,2,3,4,5,6,7或 SUN,MON,TUE,WED,THU,FRI,SAT两种,1星期天,2星期一 |
Year | , - * / | 1970-2099的整数 |
符号含义
符号 | 用途 | 示例 |
---|---|---|
* | 匹配该域的任意值 | 如在Minutes域使用*, 表示每分钟都会触发事件 |
/ | 起始时间开始触发,每隔固定时间再触发 | 如在Minutes域使用5/20,则意味着5分钟触发一次,以后每隔20分钟再次触发,如25,45等分别触发一次 |
- | 范围 | 如在Minutes域使用5-20,表示从5分到20分钟每分钟触发一次 |
, | 列出枚举值 | 如在Minutes域使用5,20,则意味着在5和20分每分钟触发一次 |
? | 只用在DayofMonth和DayofWeek,匹配该域任意值 | 因DayofMonth和DayofWeek会冲突,故要在每月20日触发调度,则不管20日到底是星期几,只能使用如下写法: 13 13 15 20 ?。即最后一位只能用?而不能用,如果使用*则表示不管星期几都会触发,实际并非如此 |
L | 只用在DayofMonth和DayofWeek,表示最后 | 如在DayofWeek域使用5L,表示只在最后的一个星期四触发 |
W | 只用在DayofMonth,在本月中、离指定日期(周一到周五)最近的有效工作日触发 | 在DayofMonth使用5W,如果5日是星期六,则将在本月中最近的工作日:星期五,即4日触发 |
LW | 连用,只用在DayofMonth,在某个月最后一个工作日 | 即最后一个星期五 |
# | 只能在DayofMonth域,确定每个月第几个星期几 | 在4#2,表示某月的第2个星期3 |
举例:
| 例 | 用途|
| – | – |
| 0 0 2 1 ? | 每月的1日02点 |
| 0 15 10 ? MON-FRI | 周一到周五每日的10:15 |
| 0 15 10 ? 6L 2002-2006 | 2002-2006年,每年最后一个周五的10:15 |
| 0 0 10,14,16 ? | 每天的10点14点16点 |
| 0 0/30 9-17 ? | 9点到17点,从0分开始,每隔30分钟执行一次 |
| 0 0 12 ? WED | 每周三的12点:00 |
| 0 0 12 ? | 每天12:00 |
| 0 14 ? | 每天14点到14:59的每一分钟都执行 |
| 0 15 10 ? * 6#3 | 每个月的第3个星期5当天的10:15:00 |
监听
TriggerListener 监听触发器触发前事件、触发后事件
Scheduler
调度器相当于一个容器,装载着任务Job和触发器Trigger。
1 | //创建计划任务抽象工厂 |
Scheduler 是个接口,定义了多个接口方法, 允许外部通过组及名称访问和控制容器中 Trigger 和 JobDetail。
Trigger 和 JobDetail 注册到 Scheduler 中, 两者在 Scheduler 中拥有各自的组及名称, 组及名称是 Scheduler 查找定位容器中某一对象的依据, Trigger 的组及名称必须唯一, JobDetail 的组和名称也必须唯一(但可以和 Trigger 的组和名称相同,因为它们是不同类型的)。
Scheduler 可以将 Trigger 绑定到某一 JobDetail 中, 这样当 Trigger 触发时, 对应的 Job 就被执行。一个 Job 可以对应多个 Trigger, 但一个 Trigger 只能对应一个 Job。
SchedulerFactory
通过 SchedulerFactory 创建一个 SchedulerFactory 实例。
SchedulerContext
Scheduler 拥有一个 SchedulerContext,保存着 Scheduler 上下文信息,Job 和 Trigger 都可以访问 SchedulerContext 内的信息。
SchedulerContext 内部通过一个 Map,以键值对的方式维护这些数据,SchedulerContext 为保存和获取数据提供了多个 put()
和 getXxx()
的方法。可以通过 Scheduler#getContext()
获取对应的 SchedulerContext 实例。
监听
SchedulerListener 听调度器开始事件、关闭事件等等,可以注册相应的监听器处理感兴趣的事件。
ThreadPool
Scheduler 使用一个线程池作为任务运行的基础设施,任务通过共享线程池中的线程提高运行效率。
Scheduler调度线程主要有两个:
- 执行常规调度的线程:轮询存储的所有trigger,如果有需要触发的trigger,即到达了下一次触发的时间,则从任务执行线程池获取一个空闲线程,执行与该trigger关联的任务
- 执行misfiredtrigger的线程:扫描所有的trigger,查看是否有misfiredtrigger,如果有的话根据misfire的策略分别处理(fire now OR wait for the next fire)。
org.quartz.simpl.SimpleThreadPool’ - with 10 threads
在Quartz中有两类线程:Scheduler调度线程和任务执行线程。
任务执行线程:Quartz不会在主线程(QuartzSchedulerThread)中处理用户的Job。Quartz把线程管理的职责委托给ThreadPool,一般的设置使用SimpleThreadPool。
SimpleThreadPool创建了一定数量的WorkerThread实例来使得Job能够在线程中进行处理。WorkerThread是定义在SimpleThreadPool类中的内部类,它实质上就是一个线程。
1 | # 默认,且默认10个线程 |
Calendars
用于触发器的触发计划中排除时间块,比如国庆期间不营业。
1 | import org.quartz.impl.calendar.HolidayCalendar; |
- AnnualCalendar 年历
- CronCalendar cron 日历
- DailyCalendar 每日日历
- HolidayCalendar 假日日历
- MonthlyCalendar 月历
- WeeklyCalendar 周历
Job Stores
Quartz中的trigger和job需要存储下来才能被使用。
主要存储内容是scheduler下的job、trigger、calender等==work data==
Quartz中有两种常用的存储方式:
- RAMJobStore:内存存储。存取速度快,停止后数据丢失。
- JDBCJobStore: 数据库存储。持久化数据,集群中只能使用持久化存储。
1. RAMJobStore(内存存储 )
【默认配置】scheduler 被关闭或机器故障后,数据就丢了
1 | org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore |
2. JDBCJobStore(数据库存储)
支持的数据库有==Oracle, PostgreSQL, MySQL, MS SQLServer, HSQLDB, and DB2==,需要创建quartz单独的数据库表,各类数据库的quartz表结构语句在quartz源代码的==“docs/dbTables”==下。参见集群相关库表。
事务类型:
- JobStoreTX(默认,常见):TM 是 transactions-managed 的缩写,该类会处理事务的提交和回滚等逻辑。
- JobStoreCMT:CMT 是 container-managed-transactions 的缩写,当前类不处理事务的提交和回滚,全权由应用服务器自身处理。quartz会让应用服务来管理quartz事务
配置quartz 使用JobStoreTX
1 | org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX |
配置数据库委托
数据库委托信息在包==org.quartz.impl.jdbcjobstore==下
1 | org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate |
quartz数据库表的前缀
1 | org.quartz.jobStore.tablePrefix = QRTZ_ |
配置数据源
可以指定==org.quartz.jobStore.useProperties==为==true==,这样的就是以==String==类型存储==JobDataMaps==信息,而不是==BLOB==
1 | org.quartz.jobStore.dataSource = myDS |
3. TerracottaJobStore(Terracotta服务器存储)
Quartz被Terracotta收购了。
TerracottaJobStore可以是集群,也可以是单点的,不过都可以为作业数据提供存储介质,因为数据存储在Terracotta服务器中,因此在应用程序重新启动期间是持久存储的。、
它的性能比JDBCJobStore使用数据库要好得多(大约好一个数量级),但是比RAMJobStore慢得多。
配置使用TerracottaJobStore
1 | org.quartz.jobStore.class = org.terracotta.quartz.TerracottaJobStore |
集群
Quartz在某一个时刻,将随机指定集群中的某一个节点,来完成任务。
- 一致性:即节点之间在同一时刻是互斥关系,通过锁机制,保证多个节点Scheduler实例对任务的调度唯一。
- 高可用性:当一个节点执行失败,可以依靠另一个节点运行。
高可用的两种配置:
当一个节点Sheduler实例执行某个Job失败时,希望由另一正常工作节点的Scheduler实例接过这个Job重新运行。则,配置给JobDetail对象的Job可恢复属性必须设为true(jobDetail.setRequestsRecovery(true))。
1
2 > <property name="requestsRecovery" value="true" />
>
当一个节点Scheduler实例执行某个Job失败时,希望Job不再重新运行,而是由另一个节点Scheduler实例在下一次触发时间触发。则,可恢复属性应该设置为false(默认为false)。
1
2 > <property name="requestsRecovery" value="false" />
>
Scheduler实例出现故障后多快能被侦测到,取决于每个Scheduler检入间隔(org.quartz.jobStore.clusterCheckinInterval)。
集群架构
Quartz集群中,每个节点是一个独立的Quartz应用,必须对每个节点分别启动或停止。
Quartz集群中,独立Quartz节点并不与其他节点通信,而是通过【相同的数据库表】感知另一Quartz应用。
- 只有使用【持久化JobStore】存储Job和Trigger才能完成Quartz集群。因此集群依赖于数据库,
JobStore的存储方式有两种:
- RAMJobStore:将scheduler存储在内存中,但是重启服务器信息会丢失。
- JDBCJobStore:将scheduler存储在数据库中。适用于集群。
因为集群依赖于数据库,所以必须采用数据库用来持久化存储scheduler数据。
数据库利用QRTZ_LOCKS表执行“行锁”(数据库悲观锁),来保证某时刻某job只能被一个节点执行。
graph TB A0(Quartz服务器节点1) --> C(Quartz数据库) A1(Quartz服务器节点2) --> C A2(Quartz服务器节点...) --> C
集群相关数据库表
集群依赖于数据库,所以首先创建Quartz数据库表。
Quartz官方提供了生成数据库表的sql语句:
表名 | 含义 |
---|---|
QRTZ_JOB_DETAILS | 记录每个任务JOB的详细信息 |
QRTZ_TRIGGERS | 记录每个触发器Trigger的详细信息 |
QRTZ_CRON_TRIGGERS | 存放cron类型的触发器 |
QRTZ_SIMPLE_TRIGGERS | 存储简单的trigger,包括重复次数,间隔,以及触发次数。 |
QRTZ_FIRED_TRIGGERS | 存储已经触发的trigger相关信息,trigger随着时间的推移状态发生变化,直到最后trigger执行完成,从表中被删除。相同的trigger和job,每触发一次都会创建一个实例;从刚被创建的ACQUIRED状态,到EXECUTING状态,最后执行完从数据库中删除; |
QRTZ_PAUSED_TRIGGER_GRPS | 存储已暂停的 Trigger 组的信息 |
QRTZ_SIMPROP_TRIGGERS | 存储CalendarIntervalTrigger和DailyTimeIntervalTrigger两种类型的触发器 |
QRTZ_BLOB_TRIGGERS | 自定义的triggers使用blog类型进行存储(非自定义的triggers不会存放在此表中,Quartz提供的triggers包括:CronTrigger,CalendarIntervalTrigger,DailyTimeIntervalTrigger以及SimpleTrigger) |
QRTZ_CALENDARS | 以 Blob 类型存储 Quartz 的 Calendar 信息 |
QRTZ_SCHEDULER_STATE | 存储集群下所有节点的scheduler实例名,会定期检查scheduler是否失效,启动多个scheduler,查询数据库。记录了最后最新的检查时间。 |
QRTZ_LOCKS | 存储程序的悲观锁的信息(假如使用了悲观锁) |
QRTZ_SCHEDULER_STATE:
1 | <bean name="quartzScheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean"> |
Quartz提供的锁表,为多个节点调度提供分布式锁,实现分布式调度,默认有2个锁:
- STATE_ACCESS:主要用在scheduler定期检查是否有效的时候,保证只有一个节点去处理已经失效的scheduler。
- TRIGGER_ACCESS:主要用在TRIGGER被调度的时候,保证只有一个节点去执行调度。
还有其他锁:
- CALENDAR_ACCESS
- JOB_ACCESS
- MISFIRE_ACCESS
sql附录
1 | DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS; |
注意问题
1. 时间同步问题
Quartz并不关心是在相同机器、还是不同的机器运行节点。
|水平集群|垂直集群|
|–|–|
|集群放置在不同的机器上|节点跑在同一台机器上|
|存在时间同步问题|存在单点故障的问题,无法真正高可用,一旦机器崩溃,全部终止|
节点用时间戳来通知其他实例它自己的最后检入时间。假如节点的时钟被设置为将来的时间,那么运行中的Scheduler将再也意识不到那个结点已经宕掉了。
另一方面,如果某个节点的时钟被设置为过去的时间,也许另一节点就会认定那个节点已宕掉并试图接过它的Job重运行。
需要同步计算机时钟:比如两者都是使用某一个Internet时间服务器(Internet Time Server ITS)
1 | yum install -y ntpdate |
2. 节点争抢Job问题
Quartz官网,当前还不存在一个方法来指派(钉住) 一个 Job 只能由集群中的某个特定节点执行。
Quartz使用一个随机的负载均衡算法,Job以随机的方式由不同的实例执行。
quartz.properties
Quartz支持配置文件,好处是比编写代码简单,且修改后不需要重新编译源码
- 文件定义了Quartz应用运行时行为,还包含了许多能控制Quartz运转的属性
- 文件应放在工程的classpath中
- 主要包括scheduler、threadPool、jobStore、plugin等
- 所有属性使用固定前缀org.quartz
1 | # Default Properties file for use by StdSchedulerFactory |
各框架使用
QuartzJobBean
无论SpringMvc还是SpringBoot都可以使用spring帮我们封装的Job抽象QuartzJobBean
。
QuartzJobBean是对Job的实现类,增加了executeInternal抽象方法。
1 | package org.springframework.scheduling.quartz; |
QuartzJobBean依赖的jar包——
1 | <!-- 在SpringMVC 中可以额外借助 --> |
SpringMVC
依赖
1 | <properties> |
xml配置
resources/ microservice-quartz.xml
Job
- JobDetailFactoryBean
- MethodInvokingJobDetailFactoryBean
Trigger
- CronTriggerFactoryBean
- SimpleTriggerFactoryBean
Scheduler
- SchedulerFactoryBean
1 |
|
xml文件引入
resources/ microservice-quartz.xml
1 | import org.springframework.context.annotation.ImportResource; |
执行类和方法
1 | import org.quartz.DisallowConcurrentExecution; |
springmvc调用@service方法
1 | // quartz bean中配置属性 |
Spring boot
依赖
1 | <!--spring boot集成quartz--> |
配置类
StoreDurably:
默认情况下,当 Job 没有对应的 Trigger 时,Job 不能直接被加入调度器,或者在 Trigger 过期之后, 没有关联 Trigger 的 Job 也会被删除。可以通过 JobBuilder 的 StoreDurably 使 Job 独立存储于调度器中。
添加job不带触发器必须写storeDurably()否则报如下异常,durable指明任务就算没有绑定Trigger仍保留在Quartz的JobStore中。
storeDurably的xml写法。
1 | <property name="durability" value="true" /> |
1 |
|
执行类和方法
使用spring抽象类QuartzJobBean
1 | public class MyJob extends QuartzJobBean { |
spring boot调用@service方法
问题描述:定时任务QuartzJobBean实现类中,@Autowired 注入service 报错NullPointerException
,即spring boot spring mvc 在QuartzJobBean实现类中,不能直接 注入service
原因:由于定时任务Quartz的job是在quartz中实例化出来的,优先级高于Spring的自动注入,创建的Service将由Quartz管理,而不是Spring,所以无法注入。不受spring的管理。所以就导致注入不进去了
办法:
- 在方法中获取bean的方式。不通过spring的@Autowired 注入service,而是直接获取spring上下文中的service类。 【参见springMvc的解决方式】
- 利用@Autowired 注入service,需要做一些配置。
- 其他方法注入service。
方法1:获取bean的方式
首先,定义一个ApplicationContextAware的组件。
1 | package com.example.demoinit.quartz.dynamic; |
而后,在servcie的实现类注解添加名字,以便bean的获取
1 | "UserService") ( |
最后,使用bean的方式拿到service
1 | public class AutoPrint extends QuartzJobBean { |
方法2:@Autowired 注入service
首先,定义一个AdaptableJobFactory的组件。
1 | package com.example.demoinit.quartz.dynamic; |
而后,在Quartz的配置文件中将job实例化,能够操作进行Spring 注入
1 | package com.example.demoinit.quartz.dynamic; |
最后,调用
1 | package com.example.demoinit.quartz.dynamic; |
动态案例
调用
1 | package com.example.demoinit.quartz.dynamic; |
增删改操作
1 | package com.example.demoinit.quartz.dynamic; |
参数
1 | package com.example.demoinit.quartz.dynamic; |
参数Job实现类
1 | package com.example.demoinit.quartz.dynamic; |
参数job调用的service层
1 | package com.example.demoinit.quartz.dynamic; |
环境配置
1 | package com.example.demoinit.quartz.dynamic; |
防止Spring依赖注入为null1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24package com.example.demoinit.quartz.dynamic;
import org.quartz.spi.TriggerFiredBundle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.scheduling.quartz.AdaptableJobFactory;
import org.springframework.stereotype.Component;
"MyAdaptableJobFactory") (
public class MyAdaptableJobFactory extends AdaptableJobFactory {
private AutowireCapableBeanFactory capableBeanFactory;
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
// 调用父类的方法
Object jobInstance = super.createJobInstance(bundle);
// 进行注入
capableBeanFactory.autowireBean(jobInstance);
return jobInstance;
}
}
Java
没有任何框架。
依赖
.quartz依赖
.quartz-jobs
1 | <!-- https://mvnrepository.com/artifact/org.quartz-scheduler/quartz --> |
1 | <!-- https://mvnrepository.com/artifact/org.quartz-scheduler/quartz-jobs --> |
配置
使用quartz接口Job
1 | // job实现类 |
配置调度和触发器
1. 可以写到main方法中执行。
1 | public static void main(String[] args) throws SchedulerException, InterruptedException { |
2. 也可以写implements CommandLineRunner的方法中,会项目启动后就自动运行
1 | package com.example.demoinit.quartz.dynamic; |
参考
Quartz框架
https://www.jianshu.com/p/2a5d3b6336ba
Quartz集群实战与原理分析
https://blog.51cto.com/simplelife/2314620
https://segmentfault.com/a/1190000009128277#item-1
https://blog.csdn.net/noaman_wgs/article/details/80984873
Xml https://zhuanlan.zhihu.com/p/46245863
简单配置属性手册 https://www.jianshu.com/p/0bf5d791d3c9
springboot 整合 quartz 实现定时任务的动态修改,启动,暂停等操作 https://hacpai.com/article/1548229459644
Springboot 2.x 整合 quartz 定时任务 实现动态添加、暂停、删除等功能 https://zzzmh.cn/single?id=83
springboot 博客 http://tigerdu.com/2018/01/16/springboot-quartz/