Quartz
- 视频作者:bilibili 灰_灰
- 学习链接
1. 简介
1.1 quartz介绍
Quartz是一套轻量级的,基于java实现的任务调度框架任务调度框架,只需要定义了 Job(任务),Trigger(触发器)和 Scheduler(调度器),即可实现一个定时调度能力。支持基于数据库的集群模式,可做到任务幂等执行
1.2 使用场景
- 电商平台达到促销时间时,修改商品价格
- 信用卡每笔消费30天后的还款提醒
- 每周五的周报提醒
- 每月最后一天的月报提醒
- … …
2. 入门
Quartz的模型:
- 一个触发器只能调度一个任务,但是一个任务可以被多个触发器调度
- 一个任务详情只能关联一个任务,但是一个任务可以被多个详情关联
2.1 入门案例
- JobBuilder下newJob进行创建任务
- HelloJob类需要实现Job接口
- TriggerBuilder下newTrigger进行创建触发器
- SimpleScheduleBuilder下simpleSchedule进行创建调度器
QuartzTest.class
package com.hyxs.share.quartz.first;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import java.util.concurrent.TimeUnit;
/**
* @author 火云勰神
* @date 2023-10-07 11:25
* @description 第一个Quartz测试类
*/
public class QuartzTest {
public static void main(String[] args) {
try {
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.start();
// newJob由JobBuilder创建
JobDetail job = JobBuilder.newJob(HelloJob.class)
.withIdentity("job1","group1")
.build();
// newTrigger由TriggerBuilder创建
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1","group1")
.startNow() //指定执行时间为当前
// simpleSchedule由SimpleScheduleBuilder创建
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
// 创建一个简单的调度策略,每隔5秒执行一次
.withIntervalInSeconds(5)
.repeatForever())
.build();
scheduler.scheduleJob(job,trigger);
TimeUnit.SECONDS.sleep(20);
scheduler.shutdown();
} catch (SchedulerException | InterruptedException e) {
e.printStackTrace();
}
}
}
HelloJob.class
- 实现Job接口
- 里面就execute一个方法
package com.hyxs.share.quartz.first;
import com.hyxs.share.quartz.utils.DateFormatUtil;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import java.util.Date;
/**
* @author 火云勰神
* @date 2023-10-07 11:42
* @description
*/
public class HelloJob implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println("HelloJob.execute " + " 时间: " + DateFormatUtil.getStrDate(new Date()) + " 当前线程:" +Thread.currentThread().getName());
}
}
执行结果
- 休眠20秒,每5秒执行一次,最终执行了五次是因为休眠开始前就已经执行了一次任务
2.2 触发器
在JobDetail和Trigger 的name和group中,group是用来标记的、方便管理的,name是用来标记的
- group不做指定时,底层会默认给一个DEFAUT 的组名
- name和group都不指定(如果就一个定时任务),同样能够运行,底层会默认生成一个MD5的name
2.2.1 触发器 任务之间的关系
一个触发器只能调度一个任务,但是一个任务可以被多个触发器调度
- 触发器 ->任务 一对一
- 任务 -> 触发器 一对多
QuartzTest2.java
public class QuartzTest2 {
public static void main(String[] args) {
try {
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.start();
JobDetail job = JobBuilder.newJob(HelloJob.class)
.withIdentity("job1","group1")
.build();
// 第一个触发器
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1","group1")
.startNow()
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(1)
.repeatForever())
.build();
// 第二个触发器
Trigger trigger2 = TriggerBuilder.newTrigger()
.withIdentity("trigger2","group1")
// 如果是同样的调度方式,不能保证每次都能拿到job,因此使用forjob
.forJob("job1","group1")
.startNow()
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(3)
.repeatForever())
.build();
scheduler.scheduleJob(job,trigger);
scheduler.scheduleJob(trigger2);
TimeUnit.SECONDS.sleep(3);
scheduler.shutdown();
} catch (SchedulerException | InterruptedException e) {
e.printStackTrace();
}
}
}
HelloJob.java
public class HelloJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
StringJoiner outStr = new StringJoiner(" ")
.add("HelloJob.execute ")
.add(" 时间: " + DateFormatUtil.getStrDate(new Date()))
.add(" 当前线程:" +Thread.currentThread().getName())
.add(" 触发器" + context.getTrigger().getKey().getName());
System.out.println("outStr = " + outStr);
}
}
执行结果:
2.2.2 CronExcepression
几类触发器:
- CronTriggerImpl(重要!!)
- SimpleTriggerImpl(上面用到的SimpleScheduleBuilder就是来自这个实现类)
- CalendarIntervalTriggerImpl
- DailyTimeIntervalTriggerImpl
cron规则:
- ” * ” 字符用于指定所有值。例如,分钟宇段中表示”每分钟”。
- “ ?” 字在 日和 星期字段允许使用。它用于指定"无特定值”。
- 即不知道是什么,和"*"是不一样的
- 例如,每个月15号 ,应该写成* * * 15 * ? *,理解为不知道每个月的15号是星期几,而不能写为"*"去让它任意匹配,到这一天星期几是固定的,只是不知道具体是星期几
- 即不知道是什么,和"*"是不一样的
- “ - “ 字符用于指定范围。例如,小时字段中的”10-12”表示”小时10、11和12”。
- “ ,” 字符用于指定其他值。例如,"星期几"字段中的“MON,WED,FRI"表示"星期一,星期三和星期五的日子”。
- " / “ 字符用于指定增量。例如,秒字段中的"0/15"表示"秒0、15、30和45”。
- 在”/"之前指定”*"等同于指定0为开头的值。本质上,对于表达式中的每个字段,都有一组可以打开或关闭的数字。
- 对于秒和分钟,数字范围为0到59,对于小时0到23,对于每月的0到31,以及对于月0到11 JAN到DEC)。因此,"月"字段中的“7/6"仅打开“7"月,并不意味着每6个月一次,最大就到11,和只写一个7没有区别
- “ L ” 字符可以在月”和“周”字段中使用。表示最后一天。
- “月”字段中的值”L“表示”月的最后一天”,即非润年的1月31日,2月28日。
- “星期”字段中,表示”7”或”SAT”(周六) ==> 最后一天是周六。
- 如果 L与其他值配合使用,则表示"该月的最后一个xxx天"
- 例如,"6L表示"该月的最后一个星期五”
- 如果 L与其他值配合使用,则表示"该月的最后一个xxx天"
- 还可以指定与该月最后一天的偏移量
- 例如“L-3”,这表示日历月的倒数第三天
- 使用“L“时,不要指定列表或值的范围,会导致混淆
- “ W ” 字符 在“日”字段中允许使用。此字符用于指定最接近给定日期的工作日(星期一至星期五)
- 如指定”15W“,则含义是:“离月15日最近的工作日”。
- 如果15号是星期六,那么触发器将在14号星期五触发,如果15日是星期日,则将在16日星期一触发
- 如”1W“为指定月份的值,而这个月的第一天是星期六,则触发器将在第3天,星期一才触发,并且不会跳过月与月的边界,即不会再上个月的周五触发
- 如指定”15W“,则含义是:“离月15日最近的工作日”。
- 还可将“L“和”W“字符组合为一个月中的一天的表达式”LW”,这表示该月的最后一个工作日
- ” # “ 字符在”星期”字段允许使用。此字符用于指定月份的"第n个”XXX天。
- “星期"字段中的“6#3”的值表示该月的第三个星期五
- 如果指定了不存在的星期数,该月将不会触发
- 如某个月只有第五个星期三,那么 "4 # 5"就不会被触发
- 如果使用“#”字符,则“星期几"字段中只能有一个表达式
- (”3 #1,6#3")无效,因为有两个表达式
- 法定字符以及月份和星期几的名称不区分大小写。
- 年是可选的,即可写可不写
注:
- 图例中月份的范围为0-11,实际使用范围1-12
- 虽然在CronExpression底层的map集合定义月份时,是0-11(这与cron表达式的设计初衷有关系) 但是最后的使用是和当前的月份取值范围 1-12一致,因为后序的逻辑中有一个取月份值然后加一的动作
2.2.3 传入变量
JobDetail和Trigger传参可以使用和job相关的方法进行传参,如果job和trigger同时传入相同的key,将会以trigger为主
JobDetail job = JobBuilder.newJob(HelloJob.class)
.usingJobData("1","2")
.withIdentity("job1","group1")
.build();
JobDataMap mergedJobDataMap = context.getMergedJobDataMap();
String value = (String)mergedJobDataMap.get("1");
System.out.println("value = " + value);
2.2.4 quartz配置文件
quartz的默认配置文件 quartz.properties,如果不指定配置文件,就会使用下面的默认配置文件
# Default Properties file for use by StdSchedulerFactory
# to create a Quartz Scheduler Instance, if a different
# properties file is not explicitly specified.
#
org.quartz.scheduler.instanceName: DefaultQuartzScheduler
org.quartz.scheduler.rmi.export: false
org.quartz.scheduler.rmi.proxy: false
org.quartz.scheduler.wrapJobExecutionInUserTransaction: false
org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount: 10
org.quartz.threadPool.threadPriority: 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true
org.quartz.jobStore.misfireThreshold: 60000
org.quartz.jobStore.class: org.quartz.simpl.RAMJobStore
具体可配置项官方文档已经给出:
2.3 集成spring
方法一:
SpringJob
public class SpringJob extends QuartzJobBean {
@Autowired
private HelloSpringService helloSpringService;
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
StringJoiner outStr = new StringJoiner(" ")
.add("SpringJob.executeInternal ")
.add(" 时间: " + DateFormatUtil.getStrDate(new Date()))
.add(" 当前线程:" +Thread.currentThread().getName())
.add(" helloService:" +helloSpringService.toString())
.add(" 方法调用:" +helloSpringService.helloSpring());
System.out.println("outStr = " + outStr);
}
}
JobInit 初始化方法
@Component
public class JobInit {
@Autowired
public Scheduler scheduler;
/**
* PostConstruct 注解的作用:
* 在spring初始化前,完成某个对象的初始化
* 如:通过Autowired注解相当于new 一个对象,但是当前想拿到一个有值的对象
* 那么此时就可以通过PostConstruct提前初始化这个对象,其他地方进行调用时,就能拿到一个有值的对象
*/
@PostConstruct
public void initJob() throws SchedulerException {
// 任务详情
JobDetail job = JobBuilder.newJob(SpringJob.class)
.build();
// 触发器
Trigger trigger = TriggerBuilder.newTrigger()
.startNow()
.build();
scheduler.scheduleJob(job,trigger);
}
}
service
@Service
public class HelloSpringService {
public String helloSpring() {
return "hello spring";
}
}
除了上述的方式,spring还提供了另外一种
方法二:
- JobDetail和Trigger的bean会自动与scheduler相关联
@Configuration
public class JobConfig {
@Bean
public JobDetail initJobDetail(){
return JobBuilder.newJob(SpringJob.class)
.withIdentity("springJob")
// 这个方法保证当前的detail没被使用时也会存在,否则通过名称查找detail会报错
.storeDurably()
.build();
}
@Bean
public Trigger initTrigger(){
return TriggerBuilder.newTrigger()
.forJob("springJob")
.startNow()
.build();
}
}
2.4 持久化
quartz框架默认的是RAM存储,也就是基于内存的存储,它可以提供很快的访问速度,但是当服务停止,数据也就会丢失,因此需要去实现持久化
将任务信息存储在数据库中,好处:
- 可以在数据库看到quartz当前有多少任务、trigger,并且方便的看出状态
- 出现异常数据时,可以帮助定位问题
配置文件:
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/quartz?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
username: root
password: 123456
quartz:
job-store-type: jdbc
jdbc:
initialize-schema: always
其中,jdbc:initialize-schema: always 的含义是,每次启动,都会执行脚本
可以在一下路径找到quartz的建表脚本
脚本的基本逻辑是 先删除表再建表,一共会创建11张表
表名称 | 实际含义 |
---|---|
QRTZ_CRON_TRIGGERS | 存储CRON类型的Trigger,包括名称、所属组、Cron表达式、时区信息 |
QRTZ_SIMPLE_TRIGGERS | 存储SIMPLE类型的Trigger,存储名称、分组、重复次数、执行时间间隔已经已经执行的次数。失火策略该表不存,存放于ORTZ_TRIGGER表中。 |
QRTZ_TRIGGERS | 存储所有的Trigger信息,包括名称、分组、类型(CRON或SIMPLE)、当前运行状态、上下次执行时间、MisFire策略 |
QRTZ_FIRED_TRIGGERS | 存储正在执行任务中的Trgigger。主要存储运行服务器节点ID、Trigger名称分组、触发器执行状态、触发与调度时间 |
QRTZ_PAUSED_TRIGGER_GRPS | 存储暂停的Trigger组 |
QRTZ_JOB_DETAILS | 存放JobDetail的具体信息。核心:JobName,JobGroup,描述,ClassName与是否独立存储等 |
QRTZ_LOCKS | 存储程序的悲观锁的信息 |
QRTZ_SCHEDULER_STATE | 存储调度器状态。上次检查时间与检测状态 |
QRTZ_CALENDARS | 存储Quartz的Calendar信息 |
QRTZ_BLOB_TRIGGERS | Trigger做为Blob类型存储 |
QRTZ_SIMPROP_TRIGGERS | 触发器相关的表 |
多数据源的方式,如上图中第二个示例,spring可以使用如下的方式:
@Bean
@QuartzDataSource
public DataSource quartzSource(){
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("");
dataSource.setUsername("");
dataSource.setUrl("");
dataSource.setPassword("");
return dataSource;
}