前言

实际开发中,我们所要执行的定时任务一定不会是固定写在代码中的内容,一定是需要我们动态增减的。
这就引出了两个问题:
● 定时任务如何持久化?
● 如何获取要添加的定时任务的内容并持久化到数据库中?


前置知识

基础概念

首先需要明白Quartz中的几个核心概念,这样才能帮助我们更好地理解Quartz的原理

  1. Job 表示一个工作,要执行的具体内容。此接口中只有一个方法:
  1. void execute(JobExecutionContext context)

image.png
2. JobDetail 表示一个具体的可执行的调度程序,Job是这个可执行调度程序所要执行的内容,另外JobDetail还包含了这个任务调度的方案和策略。
3. Trigger 触发器,代表一个调度参数的配置,表示这个任务会在什么时候被执行
4. Scheduler 表示一个调度容器,一个调度容器中可以注册多个JobDetail和Trigger。当Trigger和JobDetail组合之后,就可以被Scheduler容器进行调度了

Job和JobDetail

Jobs是很容易实现的,其内部只有一个execute方法。
在Spring整合了Quartz之后,我们可以直接通过继承Spring提供的QuartzJobBean来创建Job类
image.png

  1. package com.ssssheep.springbootquartz.job;
  2. import org.quartz.JobExecutionContext;
  3. import org.quartz.JobExecutionException;
  4. import org.springframework.scheduling.quartz.QuartzJobBean;
  5. /**
  6. * Created By Intellij IDEA
  7. *
  8. * @author Xinrui Yu
  9. * @date 2022/4/24 11:31 星期日
  10. */
  11. public class MyTestJob extends QuartzJobBean {
  12. @Override
  13. protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
  14. }
  15. }

实例化一个JobDetail是通过JobBuilder类实现的。

  1. 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中

  1. // define the job and tie it to our DumbJob class
  2. JobDetail job = newJob(DumbJob.class)
  3. .withIdentity("myJob", "group1") // name "myJob", group "group1"
  4. .usingJobData("jobSays", "Hello World!")
  5. .usingJobData("myFloatValue", 3.141f)
  6. .build();

在Job执行的过程中,也可以从JobDataMap中获取数据

  1. @PersistJobDataAfterExecution
  2. @DisallowConcurrentExecution
  3. public class MyTestJob extends QuartzJobBean {
  4. @Override
  5. protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
  6. JobDataMap map = context.getJobDetail().getJobDataMap();
  7. String name = map.getString("name");
  8. System.out.println("当前任务名称:" + context.getJobDetail().getKey());
  9. System.out.println("当前时间为:" + LocalDateTimeUtil.format(LocalDateTime.now(),"yyyy-MM-dd HH:mm:ss"));
  10. System.out.println("正在通知姓名为:" + name + "的用户进行回复");
  11. }
  12. }

如果使用的是持久化的存储机制,那么在决定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

  1. TriggerBuilder
  2. .newTrigger()
  3. .withIdentity("trigger_test_1","group_test1")
  4. .startNow()
  5. .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(40).repeatForever())
  6. .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的属性包括:

  • 开始时间
  • 结束时间
  • 重复次数
  • 重复时间间隔。

举例

指定时间开始触发,不重复

  1. SimpleTrigger trigger = (SimpleTrigger) newTrigger()
  2. .withIdentity("trigger1", "group1")
  3. .startAt(myStartTime) // some Date
  4. .forJob("job1", "group1") // identify job with name, group strings
  5. .build();

指定时间触发,每隔10秒执行一次,一共重复10次

  1. trigger = newTrigger()
  2. .withIdentity("trigger3", "group1")
  3. .startAt(myTimeToStartFiring) // if a start time is not given (if this line were omitted), "now" is implied
  4. .withSchedule(simpleSchedule()
  5. .withIntervalInSeconds(10)
  6. .withRepeatCount(10)) // note that 10 repeats will give a total of 11 firings
  7. .forJob(myJob) // identify job with handle to its JobDetail itself
  8. .build();

SimpleTrigger的misfire策略

Simple Trigger有几个misfire相关的策略,告诉Quartz当misfire发生的时候,应该如何去处理。这些策略以常量的形式,在SimpleTrigger中定义

image.png

CronTrigger

CronTrigger通常比Simple Trigger更加好用,使用的也更加广泛。如果需要基于日历的概念,而不是按照SimpleTrigger的精确制定时间间隔,那么CronTrigger会是更好的选择。

Cron表达式

举例

建立一个触发器,从每天早上的8点到下午的5点之间,间隔两分钟

  1. trigger = newTrigger()
  2. .withIdentity("trigger3", "group1")
  3. .withSchedule(cronSchedule("0 0/2 8-17 * * ?"))
  4. .forJob("myJob", "group1")
  5. .build();

建立一个触发器,将在上午10:42每天发射:

  1. trigger = newTrigger()
  2. .withIdentity("trigger3", "group1")
  3. .withSchedule(dailyAtHourAndMinute(10, 42))
  4. .forJob(myJobKey)
  5. .build();

CronTrigger的misfire策略

以下文字用来通知Quartz,当CronTrigger发生“未执行任务”的时候应该做什么

image.png

持久化

建表

在Quartz中,定时任务持久化是官方已经有给我们提供现有的方案,只需要在我们的数据库中创建对应的数据表即可。

Quartz建表语句
image.png

配置文件:

  1. org.quartz.scheduler.instanceName = JobScheduler
  2. org.quartz.scheduler.instanceId = AUTO
  3. org.quartz.scheduler.rmi.export = false
  4. org.quartz.scheduler.rmi.proxy = false
  5. org.quartz.scheduler.wrapJobExecutionInUserTransaction = false
  6. org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
  7. org.quartz.threadPool.threadCount = 20
  8. org.quartz.threadPool.threadPriority = 5
  9. org.quartz.jobStore.misfireThreshold = 3000
  10. org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
  11. org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
  12. org.quartz.jobStore.useProperties = false
  13. org.quartz.jobStore.tablePrefix = QRTZ_
  14. org.quartz.jobStore.dataSource = qzDS
  15. org.quartz.jobStore.isClustered = true
  16. org.quartz.jobStore.clusterCheckinInterval = 15000
  17. org.quartz.dataSource.qzDS.driver = com.mysql.jdbc.Driver
  18. org.quartz.dataSource.qzDS.URL = jdbc:mysql://127.0.0.1:3306/quartz?useUnicode=true&characterEncoding=UTF-8&useSSL=false
  19. org.quartz.dataSource.qzDS.user = root
  20. org.quartz.dataSource.qzDS.password = 123456
  21. org.quartz.dataSource.qzDS.maxConnections = 5
  22. org.quartz.dataSource.qzDS.validationQuery = select 0 from dual

获取定时任务具体内容

可以结合Web框架来动态地添加我们的定时任务,并通过jobDataMap来给实际执行的Job动态的传入参数。
下面结合SpringBoot框架来讲解

定义接收参数的类

  1. /**
  2. * Created By Intellij IDEA
  3. *
  4. * @author Xinrui Yu
  5. * @date 2022/4/23 20:49 星期六
  6. */
  7. @Data
  8. public class TriggerAndJobParam {
  9. // job名称
  10. private String jobName;
  11. // job的分组名
  12. private String jobGroup;
  13. // 触发器名称
  14. private String triggerName;
  15. // 触发器的分组
  16. private String triggerGroup;
  17. // Cron表达式
  18. private String cronExpression;
  19. // 被通知人的姓名
  20. private String infoUserName;
  21. }

定义Job

  1. /**
  2. * Created By Intellij IDEA
  3. *
  4. * @author Xinrui Yu
  5. * @date 2022/4/23 20:52 星期六
  6. */
  7. @PersistJobDataAfterExecution
  8. @DisallowConcurrentExecution
  9. public class MyTestJob extends QuartzJobBean {
  10. @Override
  11. protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
  12. JobDataMap map = context.getJobDetail().getJobDataMap();
  13. String name = map.getString("name");
  14. System.out.println("当前任务名称:" + context.getJobDetail().getKey());
  15. System.out.println("当前时间为:" + LocalDateTimeUtil.format(LocalDateTime.now(),"yyyy-MM-dd HH:mm:ss"));
  16. System.out.println("正在通知姓名为:" + name + "的用户进行回复");
  17. }
  18. }

定义一个controller

  1. /**
  2. * Created By Intellij IDEA
  3. *
  4. * @author Xinrui Yu
  5. * @date 2022/4/23 19:29 星期六
  6. */
  7. @RestController
  8. @RequestMapping("/job")
  9. @RequiredArgsConstructor
  10. public class JobController {
  11. private final Scheduler scheduler;
  12. @PostMapping("/add/test2")
  13. public String addJob(@RequestBody TriggerAndJobParam param) throws SchedulerException {
  14. JobDataMap jobDataMap = new JobDataMap();
  15. jobDataMap.put("name",param.getInfoUserName());
  16. JobDetail jobDetail = JobBuilder.newJob(MyTestJob.class)
  17. .withIdentity(param.getJobName(), param.getJobGroup())
  18. .setJobData(jobDataMap)
  19. .build();
  20. CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder
  21. .cronSchedule(param.getCronExpression())
  22. .withMisfireHandlingInstructionDoNothing();
  23. CronTrigger cronTrigger = TriggerBuilder.newTrigger()
  24. .withIdentity(param.getTriggerName(), param.getTriggerGroup())
  25. .withSchedule(cronScheduleBuilder)
  26. .build();
  27. System.out.println("misfire:" + cronTrigger.getMisfireInstruction());
  28. scheduler.scheduleJob(jobDetail,cronTrigger);
  29. scheduler.start();
  30. return "ok";
  31. }
  32. }

测试添加一个每10s执行一次的定时任务
image.png
定时任务可以正常执行
image.png

暂停任务

  1. @GetMapping("/pause")
  2. public String pauseJobByName(String jobGroup, String jobName,String triggerName,String triggerGroup){
  3. System.out.println(jobName);
  4. System.out.println(jobGroup);
  5. try {
  6. scheduler.resumeTrigger(TriggerKey.triggerKey(triggerName,triggerGroup));
  7. scheduler.pauseJob(JobKey.jobKey(jobName,jobGroup));
  8. } catch (SchedulerException e) {
  9. e.printStackTrace();
  10. }
  11. return "ok";
  12. }

image.png
成功暂停
image.png

重启任务

  1. @GetMapping("/restart")
  2. public String startJobByName(String jobGroup,String jobName,String triggerName,String triggerGroup) {
  3. System.out.println(jobName);
  4. System.out.println(jobGroup);
  5. try {
  6. scheduler.resumeTrigger(TriggerKey.triggerKey(triggerName,triggerGroup));
  7. scheduler.resumeJob(JobKey.jobKey(jobName,jobGroup));
  8. } catch (SchedulerException e) {
  9. e.printStackTrace();
  10. }
  11. return "ok";
  12. }

任务又可以重新进行了
image.png

删除任务

  1. @GetMapping("/delete")
  2. public String deleteJobByName(String jobName,String jobGroup){
  3. System.out.println(jobName);
  4. System.out.println(jobGroup);
  5. try {
  6. scheduler.deleteJob(JobKey.jobKey(jobName, jobGroup));
  7. } catch (SchedulerException e) {
  8. e.printStackTrace();
  9. }
  10. return "ok";
  11. }

删除之后,定时任务就不会再执行了,即使调用restart接口也不可。