前言
实际开发中,我们所要执行的定时任务一定不会是固定写在代码中的内容,一定是需要我们动态增减的。
这就引出了两个问题:
● 定时任务如何持久化?
● 如何获取要添加的定时任务的内容并持久化到数据库中?
前置知识
基础概念
首先需要明白Quartz中的几个核心概念,这样才能帮助我们更好地理解Quartz的原理
- Job 表示一个工作,要执行的具体内容。此接口中只有一个方法:
void execute(JobExecutionContext context)
2. JobDetail 表示一个具体的可执行的调度程序,Job是这个可执行调度程序所要执行的内容,另外JobDetail还包含了这个任务调度的方案和策略。
3. Trigger 触发器,代表一个调度参数的配置,表示这个任务会在什么时候被执行
4. Scheduler 表示一个调度容器,一个调度容器中可以注册多个JobDetail和Trigger。当Trigger和JobDetail组合之后,就可以被Scheduler容器进行调度了
Job和JobDetail
Jobs是很容易实现的,其内部只有一个execute
方法。
在Spring整合了Quartz之后,我们可以直接通过继承Spring提供的QuartzJobBean
来创建Job类
package com.ssssheep.springbootquartz.job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;
/**
* Created By Intellij IDEA
*
* @author Xinrui Yu
* @date 2022/4/24 11:31 星期日
*/
public class MyTestJob extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
}
}
实例化一个JobDetail是通过JobBuilder类实现的。
JobBuilder.newJob(MyTestJob.class).withIdentity("job_test1","group_test1").build();
我们在创建JobDetail的时候,将要执行的Job的类名传递给了JobDetail,所以任务调度器Scheduler就会知道我们究竟要执行哪一种类型的Job;
每一次当调度器scheduler执行Job的时候,在调用其execute()
方法的时候,都会创建该类的一个新的实例;执行完毕之后,此对象实例的引用就会被丢弃了,实例就会被GC所回收。
这种执行策略的后果就是:在Job类中,不应该定义有状态的数据属性,在Job的多次执行中,这些属性的值不会被保留下来。
那么如何在Job实例中增加属性或者相关的配置呢?答案就是:使用JobDataMap
JobDataMap
JobDataMap是可以包含不限量的(序列化的)数据对象,在Job实例执行的时候,可以使用其中保存的数据。
其是Java Map接口的一个实现,额外增加了一些便于存取基本类型的数据的方法。
在将Job加入到调度器之前,在构建JobDetail的时候,可以将数据存入到JobDataMap中
// define the job and tie it to our DumbJob class
JobDetail job = newJob(DumbJob.class)
.withIdentity("myJob", "group1") // name "myJob", group "group1"
.usingJobData("jobSays", "Hello World!")
.usingJobData("myFloatValue", 3.141f)
.build();
在Job执行的过程中,也可以从JobDataMap中获取数据
@PersistJobDataAfterExecution
@DisallowConcurrentExecution
public class MyTestJob extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
JobDataMap map = context.getJobDetail().getJobDataMap();
String name = map.getString("name");
System.out.println("当前任务名称:" + context.getJobDetail().getKey());
System.out.println("当前时间为:" + LocalDateTimeUtil.format(LocalDateTime.now(),"yyyy-MM-dd HH:mm:ss"));
System.out.println("正在通知姓名为:" + name + "的用户进行回复");
}
}
如果使用的是持久化的存储机制,那么在决定JobDataMap中存放什么数据的时候要小心序列化版本的问题。
因为JobDataMap中存储的对象都会被序列化,因此很可能会导致类版本的不一致的问题;
Job状态与并发
关于Job的状态数据(JobDataMap)和并发性,还有一些地方需要注意。在Job类上可以加上一些注解,这些注解将会影响Job的状态和并发性。
@DisallowConcurrentExecution
:将该注解加在Job类上,使Quartz不要并发地执行同一个Job定义的多个实例
如果“SalesReportJob”类上有该注解,则同一时刻仅允许执行一个“SalesReportForJoe”实例,但可以并发地执行“SalesReportForMike”类的一个实例。所以该限制是针对JobDetail的,而不是job类的。
@PersistJobDataAfterExecution
:将该注解加入到Job类上,告诉Quartz在成功执行了Job类的execute方法后,更新JobDetail中JobDataMap中的数据,使得在下一次执行的时候,其保存的是更新之后的数据,而不是更新前的旧数据。
上述两个注解,如果使用了
@PersistJobDataAfterExecution
注解,那么最好同时也使用@DisallowConcurrentExecution
来不允许两个Job实例并发执行。因为两个Job实例并发执行的过程中,JobDataMap中存储的数据很有可能是不确定的。
Triggers
实例化一个Trigger
TriggerBuilder
.newTrigger()
.withIdentity("trigger_test_1","group_test1")
.startNow()
.withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(40).repeatForever())
.build();
Trigger的公共属性
- jobKey属性:当trigger触发时被执行的job的身份;
- startTime属性:设置trigger第一次触发的时间;该属性的值是java.util.Date类型,表示某个指定的时间点;有些类型的trigger,会在设置的startTime时立即触发,有些类型的trigger,表示其触发是在startTime之后开始生效。比如,现在是1月份,你设置了一个trigger–“在每个月的第5天执行”,然后你将startTime属性设置为4月1号,则该trigger第一次触发会是在几个月以后了(即4月5号)。
- endTime属性:表示trigger失效的时间点。比如,”每月第5天执行”的trigger,如果其endTime是7月1号,则其最后一次执行时间是6月5号。
优先级(Priority)
如果有很多的Trigger或者Quartz线程池中的工作线程太少,可能造成没有足够多的资源同时触发所有的trigger;
在这种情况下,可以使用优先级来控制哪些trigger优先使用工作线程。
如果没有设置优先级,那么trigger会使用默认优先级,值为5
priority的值可以是任意整数,正数、负数都可以
错过触发(misfire instructions)
由于种种原因,并不是所有的触发器都会按照我们预期设定的时间执行对应的任务
对于未正常执行的trigger,是重新执行?还是进行忽略?这是我们需要根据实际的业务需求进行灵活处理的。
不同类型的trigger,有不同的misfire机制。
超时忍耐时间
在配置Quartz的时候,有一个属性就是 org.quartz.jobStore.misfireThreshold
,这个属性配置的时触发器超时的最大忍耐时间
超时的时间是如何计算的呢?
当一个任务原本预计的执行时间是2点钟,但是在两点的时候由于同时执行的任务过多,使得该任务未能正常执行。在2点15分的时候恢复并执行。那么超时的时间就是15分钟,也就是触发器的超时时间
org.quartz.jobStore.misfireThreshold
用来设置调度引擎对触发器超时的忍耐时间,假设忍耐时间为6000毫秒,那么只有当超时时间大于6000毫秒的时候,调度引擎才会认为是真的超时,否则就视为正常执行。
日历(Calendar)
Quartz的Calendar对象,可以在定义和存储trigger的时候与trigger进行关联。Calendar用于从trigger的调度计划中排除时间段。
比如,可以创建一个trigger,每个工作日的上午9:30执行,然后增加一个Calendar,排除掉所有的商业节日。
Simple Trigger
Simple Trigger可以满足的调度需求是: 在具体的时间点执行一次,或者在具体的时间点执行并且以指定的间隔重复执行若干次。比如,你有一个trigger,你可以设置它在2015年1月13日的上午11:23:54准时触发,或者在这个时间点触发,并且每隔2秒触发一次,一共重复5次。
根据上述描述,可以发现,SimpleTrigger的属性包括:
- 开始时间
- 结束时间
- 重复次数
- 重复时间间隔。
举例
指定时间开始触发,不重复
SimpleTrigger trigger = (SimpleTrigger) newTrigger()
.withIdentity("trigger1", "group1")
.startAt(myStartTime) // some Date
.forJob("job1", "group1") // identify job with name, group strings
.build();
指定时间触发,每隔10秒执行一次,一共重复10次
trigger = newTrigger()
.withIdentity("trigger3", "group1")
.startAt(myTimeToStartFiring) // if a start time is not given (if this line were omitted), "now" is implied
.withSchedule(simpleSchedule()
.withIntervalInSeconds(10)
.withRepeatCount(10)) // note that 10 repeats will give a total of 11 firings
.forJob(myJob) // identify job with handle to its JobDetail itself
.build();
SimpleTrigger的misfire策略
Simple Trigger有几个misfire相关的策略,告诉Quartz当misfire发生的时候,应该如何去处理。这些策略以常量的形式,在SimpleTrigger中定义
CronTrigger
CronTrigger通常比Simple Trigger更加好用,使用的也更加广泛。如果需要基于日历的概念,而不是按照SimpleTrigger的精确制定时间间隔,那么CronTrigger会是更好的选择。
Cron表达式
举例
建立一个触发器,从每天早上的8点到下午的5点之间,间隔两分钟
trigger = newTrigger()
.withIdentity("trigger3", "group1")
.withSchedule(cronSchedule("0 0/2 8-17 * * ?"))
.forJob("myJob", "group1")
.build();
建立一个触发器,将在上午10:42每天发射:
trigger = newTrigger()
.withIdentity("trigger3", "group1")
.withSchedule(dailyAtHourAndMinute(10, 42))
.forJob(myJobKey)
.build();
CronTrigger的misfire策略
以下文字用来通知Quartz,当CronTrigger发生“未执行任务”的时候应该做什么
持久化
建表
在Quartz中,定时任务持久化是官方已经有给我们提供现有的方案,只需要在我们的数据库中创建对应的数据表即可。
配置文件:
org.quartz.scheduler.instanceName = JobScheduler
org.quartz.scheduler.instanceId = AUTO
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 = 20
org.quartz.threadPool.threadPriority = 5
org.quartz.jobStore.misfireThreshold = 3000
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.useProperties = false
org.quartz.jobStore.tablePrefix = QRTZ_
org.quartz.jobStore.dataSource = qzDS
org.quartz.jobStore.isClustered = true
org.quartz.jobStore.clusterCheckinInterval = 15000
org.quartz.dataSource.qzDS.driver = com.mysql.jdbc.Driver
org.quartz.dataSource.qzDS.URL = jdbc:mysql://127.0.0.1:3306/quartz?useUnicode=true&characterEncoding=UTF-8&useSSL=false
org.quartz.dataSource.qzDS.user = root
org.quartz.dataSource.qzDS.password = 123456
org.quartz.dataSource.qzDS.maxConnections = 5
org.quartz.dataSource.qzDS.validationQuery = select 0 from dual
获取定时任务具体内容
可以结合Web框架来动态地添加我们的定时任务,并通过jobDataMap
来给实际执行的Job动态的传入参数。
下面结合SpringBoot框架来讲解
定义接收参数的类
/**
* Created By Intellij IDEA
*
* @author Xinrui Yu
* @date 2022/4/23 20:49 星期六
*/
@Data
public class TriggerAndJobParam {
// job名称
private String jobName;
// job的分组名
private String jobGroup;
// 触发器名称
private String triggerName;
// 触发器的分组
private String triggerGroup;
// Cron表达式
private String cronExpression;
// 被通知人的姓名
private String infoUserName;
}
定义Job
/**
* Created By Intellij IDEA
*
* @author Xinrui Yu
* @date 2022/4/23 20:52 星期六
*/
@PersistJobDataAfterExecution
@DisallowConcurrentExecution
public class MyTestJob extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
JobDataMap map = context.getJobDetail().getJobDataMap();
String name = map.getString("name");
System.out.println("当前任务名称:" + context.getJobDetail().getKey());
System.out.println("当前时间为:" + LocalDateTimeUtil.format(LocalDateTime.now(),"yyyy-MM-dd HH:mm:ss"));
System.out.println("正在通知姓名为:" + name + "的用户进行回复");
}
}
定义一个controller
/**
* Created By Intellij IDEA
*
* @author Xinrui Yu
* @date 2022/4/23 19:29 星期六
*/
@RestController
@RequestMapping("/job")
@RequiredArgsConstructor
public class JobController {
private final Scheduler scheduler;
@PostMapping("/add/test2")
public String addJob(@RequestBody TriggerAndJobParam param) throws SchedulerException {
JobDataMap jobDataMap = new JobDataMap();
jobDataMap.put("name",param.getInfoUserName());
JobDetail jobDetail = JobBuilder.newJob(MyTestJob.class)
.withIdentity(param.getJobName(), param.getJobGroup())
.setJobData(jobDataMap)
.build();
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder
.cronSchedule(param.getCronExpression())
.withMisfireHandlingInstructionDoNothing();
CronTrigger cronTrigger = TriggerBuilder.newTrigger()
.withIdentity(param.getTriggerName(), param.getTriggerGroup())
.withSchedule(cronScheduleBuilder)
.build();
System.out.println("misfire:" + cronTrigger.getMisfireInstruction());
scheduler.scheduleJob(jobDetail,cronTrigger);
scheduler.start();
return "ok";
}
}
测试添加一个每10s执行一次的定时任务
定时任务可以正常执行
暂停任务
@GetMapping("/pause")
public String pauseJobByName(String jobGroup, String jobName,String triggerName,String triggerGroup){
System.out.println(jobName);
System.out.println(jobGroup);
try {
scheduler.resumeTrigger(TriggerKey.triggerKey(triggerName,triggerGroup));
scheduler.pauseJob(JobKey.jobKey(jobName,jobGroup));
} catch (SchedulerException e) {
e.printStackTrace();
}
return "ok";
}
成功暂停
重启任务
@GetMapping("/restart")
public String startJobByName(String jobGroup,String jobName,String triggerName,String triggerGroup) {
System.out.println(jobName);
System.out.println(jobGroup);
try {
scheduler.resumeTrigger(TriggerKey.triggerKey(triggerName,triggerGroup));
scheduler.resumeJob(JobKey.jobKey(jobName,jobGroup));
} catch (SchedulerException e) {
e.printStackTrace();
}
return "ok";
}
任务又可以重新进行了
删除任务
@GetMapping("/delete")
public String deleteJobByName(String jobName,String jobGroup){
System.out.println(jobName);
System.out.println(jobGroup);
try {
scheduler.deleteJob(JobKey.jobKey(jobName, jobGroup));
} catch (SchedulerException e) {
e.printStackTrace();
}
return "ok";
}
删除之后,定时任务就不会再执行了,即使调用restart接口也不可。