Java定时任务在实际开发中还是用到很多的,像刷新大屏可视化数据、电商下单付款计时、发送邮件等。
实现方法大致可以分为两大类吧,
java.util.Timer是 JDK 1.3 开始就已经支持的一种定时任务的实现方式。
Timer 内部使用一个叫做 TaskQueue 的类存放定时任务,它是一个基于最小堆实现的优先级队列。TaskQueue 会按照任务距离下一次执行时间的大小将任务排序,保证在堆顶的任务最先执行。这样在需要执行任务时,每次只需要取出堆顶的任务运行即可!
Timer和TimerTask用于在后台线程中调度任务的java.util类。TimerTask负责任务的执行,Timer负责任务的调度。
Timer提供了三种定时模式:
fixed delay)fixed rate)//在当前时间往后delay个毫秒开始执行
public void schedule(TimerTask task, long delay) {...}
//在指定的time时间点执行
public void schedule(TimerTask task, Date time) {...}
- public static void main(String[] args) {
- //定义一个Timer
- Timer timer = new Timer("test-timer");
- //定义一个TimerTask
- TimerTask task = new TimerTask() {
- @Override
- public void run() {
- System.out.println("任务执行时间:" + new Date() + "------------"
- + "线程:" + Thread.currentThread().getName());
- }
- };
- long delay = 3000L;
- timer.schedule(task, delay);
- System.out.println("任务添加时间:" + new Date() + "------------"
- + "线程:" + Thread.currentThread().getName());
- }

工作方式:当达到我们指定的时间,执行一次结束
任务虽然运行结束,但进程没有被销毁。并且执行任务的线程名为我们定义的Timer的名称。我们看一下源码:
- public class Timer {
- //小顶堆,用来存放timeTask
- private final TaskQueue queue = new TaskQueue();
-
- private final TimerThread thread = new TimerThread(queue);
-
- public Timer(String name) {
- thread.setName(name);
- thread.start();
- }
- }
-
- public abstract class TimerTask implements Runnable {
- long nextExecutionTime;
- long period = 0;
- public abstract void run();
- }
TaskQueue:基于小顶堆实现,用来存放timerTaskTimerThread:任务执行线程,继承Thread类nextExecutionTime:假如任务需要多次执行表示下一次执行时间period:每次任务执行间隔时间run():我们执行任务的内容创建一个 Timer 对象就是新启动了一个线程,但是这个新启动的线程,并不是守护线程,它一直在后台运行,通过如下 可以将新启动的 Timer 线程设置为守护线程。我们可以使用以下构造方法(public Timer(boolean isDaemon)或public Timer(String name, boolean isDaemon))来设置。
Fixed Delay模式(固定间隔)//从当前时间开始delay个毫秒数开始定期执行,周期是period个毫秒数
public void schedule(TimerTask task, long delay, long period) {...}
//从指定的firstTime开始定期执行,往后每次执行的周期是period个毫秒数
public void schedule(TimerTask task, Date firstTime, long period){...}
- public static void main(String[] args) {
- Timer timer = new Timer("test-timer");
- MyTimerTask task1 = new MyTimerTask("任务1");
- MyTimerTask task2 = new MyTimerTask("任务2");
-
- long delay = 1000L;
- long period = 2000L;
- timer.schedule(task1, delay, period);
- timer.schedule(task2, new Date(), period);
- }
-
- static class MyTimerTask extends TimerTask {
- private String taskName;
-
- public MyTimerTask(String taskName) {
- this.taskName = taskName;
- }
-
- @Override
- public void run() {
- try {
- Thread.sleep(3000L);
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
- System.out.println(taskName + "执行时间:" + new Date() + "------------"
- + "线程:" + Thread.currentThread().getName());
- }
- }

工作方式:
TimerThread没有执行其他任务),如有其他任务在执行,那就需要等到其他任务执行完成才能执行period时间。根据任务运行结果来看,任务1和任务2并没有按照我们所预期的间隔2秒来执行,基本上间隔都是在6秒。而且我们注册在同一Timer的任务,都是使用同一个在同一个线程上执行。TimerTask 是以队列的方式一个一个被顺序运行的,所以执行的时间和预期的时间可能不一致,因为前面的任务可能消耗的时间较长,则后面的任务运行的时间会被延迟。延迟的任务具体开始的时间,就是依据前面任务的"结束时间" 。
Fixed Rate模式(固定速率)//从当前时间开始delay个毫秒数开始定期执行,周期是period个毫秒数
public void scheduleAtFixedRate(TimerTask task, long delay, long period) {...}
//从指定的firstTime开始定期执行,往后每次执行的周期是period个毫秒数
public void scheduleAtFixedRate(TimerTask task, Date firstTime, long period){...}
- public static void main(String[] args) {
- Timer timer = new Timer("test-timer");
- MyTimerTask task1 = new MyTimerTask("任务1");
- MyTimerTask task2 = new MyTimerTask("任务2");
-
- long delay = 1000L;
- long period = 5000L;
-
- timer.scheduleAtFixedRate(task1, delay, period);
- timer.scheduleAtFixedRate(task2, new Date(System.currentTimeMillis() - 1000L), period);
- }
-
- static class MyTimerTask extends TimerTask {
- private String taskName;
-
- public MyTimerTask(String taskName) {
- this.taskName = taskName;
- }
-
- @Override
- public void run() {
- try {
- Thread.sleep(2000L);
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
- System.out.println(taskName + "执行时间:" + new Date() + "------------"
- + "线程:" + Thread.currentThread().getName());
- }
- }

工作方式:
一般情况下和schedule()方法没有什么区别,我们可以观察结果发现任务2第一次和第二次执行相差4秒,我们设置开始时间为当前时间前1秒,scheduleAtFixedRate()当计划时间早于当前时间,则任务立即被运行。

通过上面分析,Java的定时调度可以通过Timer&TimerTask来实现。由于其实现的方式为单线程,因此从JDK1.3发布之后就一直存在一些问题,大致如下:
ScheduledExecutorService在设计之初就是为了解决Timer&TimerTask的这些问题。因为天生就是基于多线程机制,所以任务之间不会相互影响(只要线程数足够。当线程数不足时,有些任务会复用同一个线程)。
除此之外,因为其内部使用的延迟队列,本身就是基于等待/唤醒机制实现的,所以CPU并不会一直繁忙。同时,多线程带来的CPU资源复用也能极大地提升性能。

因为ScheduledExecutorService继承于ExecutorService,所以本身支持线程池的所有功能。额外还提供了4种方法,我们来看看其作用。
- /**
- * 带延迟时间的调度,只执行一次
- * 调度之后可通过Future.get()阻塞直至任务执行完毕
- */
- 1. public ScheduledFuture> schedule(Runnable command,
- long delay, TimeUnit unit);
-
- /**
- * 带延迟时间的调度,只执行一次
- * 调度之后可通过Future.get()阻塞直至任务执行完毕,并且可以获取执行结果
- */
- 2. public
ScheduledFuture schedule(Callable callable, - long delay, TimeUnit unit);
-
- /**
- * 带延迟时间的调度,循环执行,固定频率
- */
- 3. public ScheduledFuture> scheduleAtFixedRate(Runnable command,
- long initialDelay,
- long period,
- TimeUnit unit);
-
- /**
- * 带延迟时间的调度,循环执行,固定延迟
- */
- 4. public ScheduledFuture> scheduleWithFixedDelay(Runnable command,
- long initialDelay,
- long delay,
- TimeUnit unit);
具体不多做分析了,可以理解为Time的多线程版。
Spring Task、是Spring3.0内置的定时任务框架,支持Cron表达式来指定定时任务执行时间。
下面介绍在SpringBoot中使用Spring Task。
使用SpringBoot创建定时任务非常简单,目前主要有以下三种创建方式:
@Scheduled注解和@EnableScheduling注解的使用
基于注解@Scheduled默认为单线程,开启多个任务时,任务的执行时机会受上一个任务执行时间的影响。
- @SpringBootApplication
- @EnableScheduling //开启定时任务
- public class ScheduledDemoApplication
- {
- public static void main(String[] args)
- {
- SpringApplication.run(ScheduledDemoApplication.class, args);
- }
- }
-
-
-
- /**
- * 创建定时任务,并使用 @Scheduled 注解。
- * @author pan_junbiao
- **/
- @Component
- public class Task
- {
- @Scheduled(cron="0/5 * * * * ? ") //每5秒执行一次
- public void execute(){
- SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //设置日期格式
- System.out.println("欢迎访问 pan_junbiao的博客 " + df.format(new Date()));
- }
- }
@Scheduled注解各参数讲解
源码如下:
- package org.springframework.scheduling.annotation;
-
- import java.lang.annotation.Documented;
- import java.lang.annotation.ElementType;
- import java.lang.annotation.Repeatable;
- import java.lang.annotation.Retention;
- import java.lang.annotation.RetentionPolicy;
- import java.lang.annotation.Target;
-
- @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
- @Retention(RetentionPolicy.RUNTIME)
- @Documented
- @Repeatable(Schedules.class)
- public @interface Scheduled {
- String CRON_DISABLED = "-";
-
- String cron() default "";
-
- String zone() default "";
-
- long fixedDelay() default -1L;
-
- String fixedDelayString() default "";
-
- long fixedRate() default -1L;
-
- String fixedRateString() default "";
-
- long initialDelay() default -1L;
-
- String initialDelayString() default "";
- }
(1)cron
该参数接收一个cron表达式,cron表达式是一个字符串,字符串以5或6个空格隔开,分开共6或7个域,每一个域代表一个含义。
cron 表达式语法:
格式:[秒] [分] [小时] [日] [月] [周] [年]
![]()
* 表示所有值. 例如:在分的字段上设置 "*",表示每一分钟都会触发。
? 表示不指定值。使用的场景为不需要关心当前设置这个字段的值。例如:要在每月的10号触发一个操作,但不关心是周几,所以需要周位置的那个字段设置为"?" 具体设置为 0 0 0 10 * ?
- 表示区间。例如 在小时上设置 "10-12",表示 10,11,12点都会触发。
, 表示指定多个值,例如在周字段上设置 "MON,WED,FRI" 表示周一,周三和周五触发
/ 用于递增触发。如在秒上面设置"5/15" 表示从5秒开始,每增15秒触发(5,20,35,50)。在月字段上设置'1/3'所示每月1号开始,每隔三天触发一次。
L 表示最后的意思。在日字段设置上,表示当月的最后一天(依据当前月份,如果是二月还会依据是否是润年[leap]), 在周字段上表示星期六,相当于"7"或"SAT"。如果在"L"前加上数字,则表示该数据的最后一个。例如在周字段上设置"6L"这样的格式,则表示“本月最后一个星期五"
W 表示离指定日期的最近那个工作日(周一至周五). 例如在日字段上设置"15W",表示离每月15号最近的那个工作日触发。如果15号正好是周六,则找最近的周五(14号)触发, 如果15号是周未,则找最近的下周一(16号)触发.如果15号正好在工作日(周一至周五),则就在该天触发。如果指定格式为 "1W",它则表示每月1号往后最近的工作日触发。如果1号正是周六,则将在3号下周一触发。(注,"W"前只能设置具体的数字,不允许区间"-").
# 序号(表示每月的第几个周几),例如在周字段上设置"6#3"表示在每月的第三个周六.注意如果指定"#5",正好第五周没有周六,则不会触发该配置(用在母亲节和父亲节再合适不过了)
可通过在线生成Cron表达式的工具:在线Cron表达式生成器 来生成自己想要的表达式。
cron表达式使用占位符
另外,cron属性接收的cron表达式支持占位符。eg:
配置文件:
time:
cron: */5 * * * * *
interval: 5
每5秒执行一次:
- @Scheduled(cron="${time.cron}")
- void testPlaceholder1() {
- System.out.println("Execute at " + System.currentTimeMillis());
- }
-
- @Scheduled(cron="*/${time.interval} * * * * *")
- void testPlaceholder2() {
- System.out.println("Execute at " + System.currentTimeMillis());
- }
(2)zone
时区,接收一个 java.util.TimeZone#ID。cron表达式会基于该时区解析。默认是一个空字符串,即取服务器所在地的时区。比如我们一般使用的时区Asia/Shanghai。该字段我们一般留空。
(3)fixedDelay
上一次执行完毕时间点之后多长时间再执行。如:
@Scheduled(fixedDelay = 5000) //上一次执行完毕时间点之后5秒再执行
(4)fixedDelayString
与 fixedDelay 意思相同,只是使用字符串的形式。唯一不同的是支持占位符。如:
@Scheduled(fixedDelayString = "5000") //上一次执行完毕时间点之后5秒再执行
占位符的使用:
在 application.yml 配置文件中添加如下配置:
time:
fixedDelay: 5000
- /**
- * 定时任务的使用
- * @author pan_junbiao
- **/
- @Component
- public class Task
- {
- @Scheduled(fixedDelayString = "${time.fixedDelay}")
- void testFixedDelayString()
- {
- System.out.println("欢迎访问 pan_junbiao的博客 " + System.currentTimeMillis());
- }
- }
(5)fixedRate
上一次开始执行时间点之后多长时间再执行。如:
@Scheduled(fixedRate = 5000) //上一次开始执行时间点之后5秒再执行
(6)fixedRateString
与 fixedRate 意思相同,只是使用字符串的形式。唯一不同的是支持占位符。
(7) initialDelay
第一次延迟多长时间后再执行。如:
@Scheduled(initialDelay=1000, fixedRate=5000) //第一次延迟1秒后执行,之后按fixedRate的规则每5秒执行一次
(8) initialDelayString
与 initialDelay 意思相同,只是使用字符串的形式。唯一不同的是支持占位符。
(1)在MySQL数据库中创建 cron 表,并添加数据。
DROP TABLE IF EXISTS cron;
CREATE TABLE cron (
cron_id VARCHAR(30) NOT NULL PRIMARY KEY,
cron VARCHAR(30) NOT NULL
);
INSERT INTO cron VALUES ('1', '0/5 * * * * ?');
(2)添加pom.xml配置信息
在pom.xml配置文件中添加MyBatis、 MySQL的JDBC数据库驱动依赖。
- <dependency>
- <groupId>org.mybatis.spring.bootgroupId>
- <artifactId>mybatis-spring-boot-starterartifactId>
- <version>2.1.3version>
- dependency>
-
- <dependency>
- <groupId>mysqlgroupId>
- <artifactId>mysql-connector-javaartifactId>
- <version>8.0.20version>
- dependency>
(3)配置相关信息
将项目默认的application.properties文件的后缀修改为“.yml”,即配置文件名称为:application.yml,并配置以下信息:
- spring:
- #DataSource数据源
- datasource:
- url: jdbc:mysql://localhost:3306/db_admin?useSSL=false&
- username: root
- password: 123456
- driver-class-name: com.mysql.cj.jdbc.Driver
-
- #MyBatis配置
- mybatis:
- type-aliases-package: com.pjb.entity #别名定义
- configuration:
- log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #指定 MyBatis 所用日志的具体实现,未指定时将自动查找
- map-underscore-to-camel-case: true #开启自动驼峰命名规则(camel case)映射
- lazy-loading-enabled: true #开启延时加载开关
- aggressive-lazy-loading: false #将积极加载改为消极加载(即按需加载),默认值就是false
- lazy-load-trigger-methods: "" #阻挡不相干的操作触发,实现懒加载
- cache-enabled: true #打开全局缓存开关(二级环境),默认值就是true
4)创建定时器
数据库准备好数据之后,我们编写定时任务,注意这里添加的是TriggerTask,目的是循环读取我们在数据库设置好的执行周期,以及执行相关定时任务的内容。具体代码如下:
- /**
- * 动态定时任务配置类
- * @author pan_junbiao
- **/
- @Configuration //1.主要用于标记配置类,兼备Component的效果
- @EnableScheduling //2.开启定时任务
- public class DynamicScheduleConfigurer implements SchedulingConfigurer
- {
- @Mapper
- public interface CronMapper {
- @Select("select cron from cron limit 1")
- public String getCron();
- }
-
- //注入mapper
- @Autowired
- @SuppressWarnings("all")
- CronMapper cronMapper;
-
- /**
- * 执行定时任务.
- */
- @Override
- public void configureTasks(ScheduledTaskRegistrar taskRegistrar)
- {
- taskRegistrar.addTriggerTask(
- //1.添加任务内容(Runnable)
- () -> System.out.println("欢迎访问 pan_junbiao的博客: " + LocalDateTime.now().toLocalTime()),
- //2.设置执行周期(Trigger)
- triggerContext -> {
- //2.1 从数据库获取执行周期
- String cron = cronMapper.getCron();
- //2.2 合法性校验.
- if (StringUtils.isEmpty(cron)) {
- // Omitted Code ..
- }
- //2.3 返回执行周期(Date)
- return new CronTrigger(cron).nextExecutionTime(triggerContext);
- }
- );
- }
- }
-
- /**
- * 基于注解设定多线程定时任务
- * @author pan_junbiao
- */
- @Component
- @EnableScheduling // 1.开启定时任务
- @EnableAsync // 2.开启多线程
- public class MultithreadScheduleTask
- {
- @Async
- @Scheduled(fixedDelay = 1000) //间隔1秒
- public void first() throws InterruptedException {
- System.out.println("第一个定时任务开始 : " + LocalDateTime.now().toLocalTime() + "\r\n线程 : " + Thread.currentThread().getName());
- System.out.println();
- Thread.sleep(1000 * 10);
- }
-
- @Async
- @Scheduled(fixedDelay = 2000)
- public void second() {
- System.out.println("第二个定时任务开始 : " + LocalDateTime.now().toLocalTime() + "\r\n线程 : " + Thread.currentThread().getName());
- System.out.println();
- }
- }
注意:由于基于注解@Scheduled默认为单线程,开启多个任务时,任务的执行时机会受上一个任务执行时间的影响。所以这里使用 @Async 注解很关键。

从控制台可以看出,第一个定时任务和第二个定时任务互不影响;
并且,由于开启了多线程,第一个任务的执行时间也不受其本身执行时间的限制,所以需要注意可能会出现重复操作导致数据异常。
上面提到了一些定时任务的解决方案都是在单机下执行的,当遇到一些复杂场景例如分布式下的分片和高可用的话,就需要用到分布式定时框架。
通常情况下,一个定时任务要涉及到以下三个角色
介绍一下常见的一些分布式定时任务框架
| QuartZ | xxl-job | SchedulerX 2.0 | PowerJob | |
| 推荐度 | 1 | 4 | 2 | 3 |
| 是否有前端页面 | N | Y | Y | Y |
| 定时类型 | CRON | CRON | CRON、固定频率、固定延迟、OpenAPI | CRON、固定频率、固定延迟、OpenAPI |
| 支持数据库 | 关系型数据库 (MySQL、Oracle...) | MySQL | 人民币(不开源) | 任意 Spring Data Jpa支持的关系型数据库(MySQL、Oracle...) |
| 报警监控 | 无 | 邮件 | 短信 | 邮件,提供接口允许开发者扩展 |
| 指定调度类型 | 不确定 | 支持 | 不确定 | 不支持 |
| 开发方式 | Bean里的方法上加注解 | 略复杂:单个Bean实现PowerJob的指定接口。 | ||
| 任务类型 | 内置Java | 内置Java、GLUE Java、Shell、Python等脚本 | 内置Java、外置Java(FatJar)、Shell、Python等脚本 | 内置Java、外置Java(容器)、Shell、Python等脚本 |
| 分布式任务 | 无 | 静态分片 | MapReduce 动态分片 | MapReduce 动态分片 |
| 在线任务治理 | 不支持 | 支持 | 支持 | 支持 |
| 日志白屏化 | 不支持 | 支持 | 不支持 | 支持 |
| 调度方式和性能 | 基于数据库锁 有性能瓶颈 | 基于数据库锁 有性能瓶颈 | 不详 | 无锁化设计,性能强劲无上限 |
| DGA工作流 | 不支持 | 不支持 | 不支持 | 不支持 |
Job:一个函数式接口,其中的execute方法就是我们需要具体实现的业务任务逻辑。
JobDetail:用于绑定Job,并对Job进行描述,其中提供了很多描述性属性如:name 任务名称、group 任务组、description 任务描述、jobClass 任务类、jobDataMap 任务自定义参数等。
Tigger:触发器,用于定义Job的执行时间、执行间隔、执行频率等。在Quartz中主要有四种类型的Trigger:SimpleTrigger、CronTrigger、DataIntervalTrigger和NthIncludedTrigger。
Scheduler:调度器,用于实际协调和组织JobDetail与Trigger。Quartz提供了DirectSchedulerFactory和StdSchedulerFactory等工厂类,用于支持Scheduler相关对象的产生。
Scheduler可看成是一个定时任务调度容器,里面可注入多组任务(JobDetail与Trigger),而每个JobDetail又绑定了一个Job实例。一个JobDetail可对应多个Trigger,一个Trigger只能对应一个JobDetail。

Quartz中主要存在两类线程:即执行线程和调度线程。
执行线程通常由一个线程池维护,主要作用是执行Trigger中即将开始的任务。
调度线程又分为Regular Scheduler Thread(执行常规调度)和Misfire Scheduler Thread(执行错失的任务)。
其中Regular Thread 轮询Trigger,如果有将要触发的Trigger,则从执行任务线程池中获取一个空闲线程,然后执行与该Trigger关联的job;
Misfire Thraed则是扫描所有的trigger,查看是否有错失的,如果有的话,根据一定的策略进行处理。
ClusterManager线程:Quartz集群部署时,则还存在集群线程(ClusterManager线程),主要作用是定时检测集群中各节点健康状态。若发现宕机节点,则将其任务交由其他健康节点继续执行。

Quartz默认加载工程目录下的quartz.properties,如果工程目录下没有,就会去加载quartz.jar包下面的quartz.properties文件,也可自定义配置位置。
配置属性大体可分为:
以整合springboot为例,贴出核心配置:
- # 定时任务的表前缀
- org.quartz.jobStore.tablePrefix=qrtz_
- # 是否是集群的任务
- org.quartz.jobStore.isClustered=true
- # 检查集群的状态间隔
- org.quartz.jobStore.clusterCheckinInterval=5000
- # 如果当前的执行周期被错过 任务持有的时长超过此时长则认为任务过期,单位ms
- org.quartz.jobStore.misfireThreshold=6000
- # 事务的隔离级别 推荐使用默认级别 设置为true容易造成死锁和不可重复读的一些事务问题
- org.quartz.jobStore.txIsolationLevelSerializable=false
- # 任务存储方式它应当是org.quartz.spi.JobStore的子类
- org.quartz.jobStore.class=org.springframework.scheduling.quartz.LocalDataSourceJobStore
- # 保证待执行的任务是锁定的 避免集群任务被其他现场抢断
- org.quartz.jobStore.acquireTriggersWithinLock=true
- # 数据库系统的方言StdJDBCDelegate标准JDBC方言
- org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
- # 定时任务实例的id 默认自动
- org.quartz.scheduler.instanceId=AUTO
- # 定时任务的线程名,相同集群实例名称必须相同
- org.quartz.scheduler.instanceName=ClusterJMVCScheduler
- # 定时任务线程池
- org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
- # 线程池的线程总数 默认10
- org.quartz.threadPool.threadCount=10
- # 线程池的优先级
- org.quartz.threadPool.threadPriority=5
- # 自创建父线程
- org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread=true
-
针对CronTrigger和SimpleTrigger过失策略分别如下:
(1)CronTrigger
(2)SimpleTrigger
(3)核心策略枚举说明
(4)默认策略
CronTrigger和SimpleTrigger默认采用MISFIRE_INSTRUCTION_SMART_POLICY大致意思是“把处理逻辑交给聪明的Quartz去决定”。基本策略是
以Quartz和spring整合为例,当spring容器启动时,就会装载相关的bean。SchedulerFactoryBean实现了InitializingBean接口,因此在初始化bean的时候,会执行afterPropertiesSet方法,该方法将会调用SchedulerFactory(DirectSchedulerFactory 或者 StdSchedulerFactory,通常用StdSchedulerFactory)创建Scheduler。
SchedulerFactory在创建quartzScheduler的过程中,将会读取配置参数,初始化各个组件,关键组件如下:
- org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
- org.quartz.threadPool.threadCount=3
- org.quartz.threadPool.threadPriority=5
另外,SchedulerFactoryBean还实现了SmartLifeCycle接口,因此初始化完成后,会执行start()方法,该方法将主要会执行以下的几个动作:
Quartz持久化即将trigger和job基于jdbc存入数据库。Quartz中有两种存储方式:RAMJobStore,JobStoreSupport,其中RAMJobStore是将trigger和job存储在内存中,而JobStoreSupport是基于jdbc将trigger和job存储到数据库中。RAMJobStore的存取速度非常快,但是由于其在系统被停止后所有的数据都会丢失,所以在集群应用中,必须使用JobStoreSupport。
集成时,执行去Quartz官网下载对应数据库sql文件导入并开启Quartz持久化配置即可:

Quartz集群是基于数据库实现,主要利用了数据库的悲观锁机制。一个Quartz集群中的每个节点是一个独立的Quartz应用,它又管理着其他的节点。这就意味着你必须对每个节点分别启动或停止。Quartz集群中,独立的Quartz节点并不与另一其的节点或是管理节点通信,而是通过相同的数据库表来感知到另一Quartz应用的。
在大型分布式系统中,为了避免Quartz集群表和业务表之间互相影响,导致数据库性能和Quartz集群、业务系统稳定性,建议是Quartz独立出数据库或独立出定时任务系统。

(1)Job无法注入spring容器其他bean
Quartz中每次执行任务时,会由JobFactory重新创建一个新Job实例,此实例默认采用反射newInstance创建且并未交给spring管理,所以在实例化时也无法注入其他spring bean。
可通过自定JobFactory方式解决,当然在与springboot整合时,QuartzAutoConfiguration自动配置类已经帮我们处理了。
(2)集群环境下时间同步问题
Quartz实际并不关心你是在相同还是不同的机器上运行节点。当集群放置在不同的机器上时,称之为水平集群。节点跑在同一台机器上时,称之为垂直集群。对于垂直集群,存在着单点故障的问题。这对高可用性的应用来说是无法接受的,因为一旦机器崩溃了,所有的节点也就被终止了。对于水平集群,存在着时间同步问题。
节点用时间戳来通知其他实例它自己的最后检入时间。假如节点的时钟被设置为将来的时间,那么运行中的Scheduler将再也意识不到那个结点已经宕掉了。另一方面,如果某个节点的时钟被设置为过去的时间,也许另一节点就会认定那个节点已宕掉并试图接过它的Job重运行。最简单的同步计算机时钟的方式是使用某一个Internet时间服务器(Internet Time Server ITS)。
(3)节点争抢Job问题
因为Quartz使用了一个随机的负载均衡算法,Job以随机的方式由不同的实例执行。Quartz官网上提到当前,还不存在一个方法来指派(钉住) 一个 Job 到集群中特定的节点。
(4)从集群获取Job列表问题
当前,如果不直接进到数据库查询的话,还没有一个简单的方式来得到集群中所有正在执行的Job列表。请求一个Scheduler实例,将只能得到在那个实例上正运行Job的列表。Quartz官网建议可以通过写一些访问数据库JDBC代码来从相应的表中获取全部的Job信息。
基于mysql数据库搭建Quartz集群,并整合springboot、mybatisplus实现一个轻量企业级定时任务框架。
(1)自定义JOB注解,并设置启动装载所有job
- package com.zkc.quartzdemo.annotation;
-
- import java.lang.annotation.ElementType;
- import java.lang.annotation.Retention;
- import java.lang.annotation.RetentionPolicy;
- import java.lang.annotation.Target;
-
- /**
- * @author kczhang@wisedu.com
- * @version 1.0.0
- * @since 2021-04-15
- */
- @Target({ElementType.TYPE})
- @Retention(RetentionPolicy.RUNTIME)
- public @interface ScheduleAnn {
-
- /**
- * 定时任务的名称
- */
- String name() default "";
-
- /**
- * 定时任务的定时表达式
- */
- String cronExpression() default "";
-
- /**
- * 定时任务所属群组
- */
- String group() default "";
-
- /**
- * 当前定时任务的描述
- */
- String description() default "";
-
- }
- package com.zkc.quartzdemo.config;
-
- import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
-
- /**
- * @author kczhang@wisedu.com
- * @version 1.0.0
- * @since 2021-04-15
- */
- @Configuration
- public class QuartzConfig {
-
- @Bean(initMethod = "init")
- @ConditionalOnMissingBean
- public QuartzInit bootStarter() {
- return new QuartzInit();
- }
-
- }
-
- package com.zkc.quartzdemo.config;
-
- import com.zkc.quartzdemo.annotation.ScheduleAnn;
- import com.zkc.quartzdemo.dto.ScheduleJobVO;
- import com.zkc.quartzdemo.service.QuartzService;
- import lombok.RequiredArgsConstructor;
- import lombok.extern.slf4j.Slf4j;
- import org.quartz.Job;
- import org.reflections.Reflections;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.beans.factory.annotation.Value;
-
- import java.util.List;
- import java.util.regex.Pattern;
-
- /**
- * @author kczhang@wisedu.com
- * @version 1.0.0
- * @since 2021-04-15
- */
- @Slf4j
- public class QuartzInit {
-
- @Value("${spring.quartz.package-scan:com.zkc}")
- private String packageScan;
- private final static Pattern expressionPattern = Pattern.compile("\\$\\{(.*)}");
- @Autowired
- private QuartzService quartzService;
-
- public void init() {
- List
allJobClassNames = quartzService.getAllScheduleJobClassNames(); - new Reflections(packageScan).getTypesAnnotatedWith(ScheduleAnn.class).forEach(jobClass -> {
- String jobClassName = jobClass.getName();
- if (allJobClassNames.contains(jobClassName)) return;
- // 如果当前jobClass不是org.quartz.Job的子类则不做插入任务操作
- if (!Job.class.isAssignableFrom(jobClass)) {
- log.error("类[{}]未继承[org.quartz.Job]无法初始化为定时任务。", jobClassName);
- return;
- }
- ScheduleAnn annotate = jobClass.getAnnotation(ScheduleAnn.class);
- String name = annotate.name();
- String cronExpression = annotate.cronExpression();
- String group = annotate.group();
- String description = annotate.description();
- ScheduleJobVO scheduleJobVO = new ScheduleJobVO()
- .setClassName(jobClassName)
- .setName(name)
- .setCron(cronExpression)
- .setGroup(group)
- .setDescription(description);
- // 只创建数据库中不存在的定时任务 存在的则不做创建更新
- boolean existsFlag = quartzService.scheduleExists(name, group);
- if (!existsFlag) {
- quartzService.addJob(scheduleJobVO);
- quartzService.addInitJob(jobClassName);
- }
- });
- }
-
- }
-
(2)定义调度servcie 与 controller
- package com.zkc.quartzdemo.service;
-
- import com.zkc.quartzdemo.dto.*;
-
- import java.util.List;
-
- /**
- * @author kczhang@wisedu.com
- * @version 1.0.0
- * @since 2021-04-15
- */
- public interface QuartzService {
-
- List
getAllScheduleJobClassNames(); -
- boolean scheduleExists(String name, String group);
-
- void addInitJob(String jobClassName);
-
- boolean addJob(ScheduleJobVO scheduleJobVO);
-
- boolean addDynamicScheduleJob(ScheduleAddCO scheduleAddCO);
-
- List
getAllJobGroups(); -
- List
getJobDetails(); -
- boolean updateScheduleJob(ScheduleUpdateCO scheduleUpdateCO);
-
- boolean deleteDynamicScheduleById(String id);
-
- List
getScheduleJobs(ScheduleQryCO scheduleQryCo); -
- boolean changeScheduleJobStatus(ScheduleStatusCO statusCO);
-
- List
getSystemAllJobs(); -
- }
-
- package com.zkc.quartzdemo.controller;
-
- import com.zkc.quartzdemo.common.MultiResponse;
- import com.zkc.quartzdemo.common.SingleResponse;
- import com.zkc.quartzdemo.dto.*;
- import com.zkc.quartzdemo.service.QuartzService;
- import lombok.RequiredArgsConstructor;
- import org.springframework.validation.annotation.Validated;
- import org.springframework.web.bind.annotation.*;
-
- import javax.validation.Valid;
- import javax.validation.constraints.NotEmpty;
-
- /**
- * @author kczhang@wisedu.com
- * @version 1.0.0
- * @since 2021-04-15
- */
- @RestController
- @RequiredArgsConstructor
- @RequestMapping("/admin/api/schedule_job")
- @Validated
- public class ScheduleController {
-
- private final QuartzService quartzService;
-
- /**
- * 添加一个新的定时任务
- *
- * @param scheduleAddCO 任务详情
- * @return 任务添加成功与否
- */
- @PostMapping(value = "/create")
- public SingleResponse
addNewSchedule(@Valid @RequestBody ScheduleAddCO scheduleAddCO) { - return SingleResponse.of(quartzService.addDynamicScheduleJob(scheduleAddCO));
- }
-
- /**
- * 获取任务的组集合
- *
- * @return 组s
- */
- @GetMapping(value = "/groups")
- public MultiResponse
findAllScheduleJobGroups() { - return MultiResponse.ofWithoutTotal(quartzService.getAllJobGroups());
- }
-
- /**
- * 更新定时任务
- *
- * @param scheduleUpdateC0 任务详情
- * @return 任务添加成功与否
- */
- @PostMapping(value = "/update")
- public SingleResponse
updateScheduleJob(@Valid @RequestBody ScheduleUpdateCO scheduleUpdateC0) { - return SingleResponse.of(quartzService.updateScheduleJob(scheduleUpdateC0));
- }
-
- /**
- * 删除定时任务
- *
- * @param id 任务的id
- */
- @GetMapping(value = "/remove")
- public SingleResponse
deleteScheduleJob(@NotEmpty String id) { - return SingleResponse.of(quartzService.deleteDynamicScheduleById(id));
- }
-
- /**
- * 获取定时任务列表
- *
- * @param scheduleQryCO 搜索条件
- * @return 定时任务列表
- */
- @GetMapping(value = "/list")
- public MultiResponse
getScheduleJobs(ScheduleQryCO scheduleQryCO) { - return MultiResponse.ofWithoutTotal(quartzService.getScheduleJobs(scheduleQryCO));
- }
-
- /**
- * 更新定时任务状态
- *
- * @param statusCO 任务状态
- * @return 定时任务状态更新成功与否
- */
- @PostMapping(value = "/updateScheduleJobStatus")
- public SingleResponse
changeScheduleJobStatus(@Valid @RequestBody ScheduleStatusCO statusCO) { - return SingleResponse.of(quartzService.changeScheduleJobStatus(statusCO));
- }
-
- /**
- * 任务集合
- *
- * @return 继承Job的SpringBean集合信息
- */
- @GetMapping(value = "/jobs")
- public MultiResponse
getSystemAllJobs() { - return MultiResponse.ofWithoutTotal(quartzService.getSystemAllJobs());
- }
-
- }
-
(3)除了quartz官方持久化表,新增一个job初始化表
DROP TABLE IF EXISTS `qrtz_auto_initialized`;
CREATE TABLE `qrtz_auto_initialized` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`job_class_name` varchar(500) DEFAULT NULL,
`init_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;
完整代码已分享到码云
https://gitee.com/zhang_kaicheng/quartz-demo
Quartz从入门到精通(最详细基础-进阶-实战)_quartz 学习-CSDN博客