① JDK定时器:Timer
理论基础:时间轮算法
- 链表或者数组实现时间轮:while-true-sleep
遍历数组,每个下标放置一个链表,链表节点放置任务,遍历到了就取出执行
缺陷:假设数组长度12代表小时,那么我想13点执行,就不太方便 - round型时间轮:任务上记录一个round,遍历到了就将round减一,为0时取出执行
需要遍历所有的任务,效率较低 - 分层时间轮:使用多个不同维度的轮
天轮:记录几点执行
月轮:记录记号执行
月轮遍历到了,将任务去除放到天轮里面,就可以实现几号几点执行
/**
* @author cVzhanshi
* @create 2022-11-02 20:27
*/
public class TimerTest {
public static void main(String[] args) {
Timer timer = new Timer(); // 任务启动
for (int i = 0; i < 2; i++) {
TimerTask timerTask = new FooTimerTask("cvzhanshi" + i);
// 任务添加 第二个参数,启动时间 第三个参数,执行间隔时间
timer.schedule(timerTask , new Date() , 2000);
// 预设的执行时间nextExecutTime 12:00:00 12:00:02 12:00:04
//schedule 真正的执行时间 取决上一个任务的结束时间 ExecutTime 03 05 08 丢任务(少执行了次数)
//scheduleAtFixedRate 严格按照预设时间 12:00:00 12:00:02 12:00:04(执行时间会乱)
//单线程 任务阻塞 任务超时
}
}
}
执行源码解析
// Timer类中的两个常量
// 任务队列
private final TaskQueue queue = new TaskQueue();
// 执行任务的线程
private final TimerThread thread = new TimerThread(queue);
- new Timer();
// 进入构造器
public Timer() {
// 调用了另一个构造器
this("Timer-" + serialNumber());
}
public Timer(String name) {
thread.setName(name);
// 启动线程执行线程的run方法
thread.start();
}
- 线程的run()方法
public void run() {
try {
// 主要方法 后面再说
mainLoop();
} finally {
...
}
}
- timer.schedule(timerTask , new Date() , 2000);
public void schedule(TimerTask task, Date firstTime, long period) {
if (period <= 0)
throw new IllegalArgumentException("Non-positive period.");
sched(task, firstTime.getTime(), -period);
}
private void sched(TimerTask task, long time, long period) {
if (time < 0)
throw new IllegalArgumentException("Illegal execution time.");
// 充分限制周期值,以防止数字溢出,同时仍然有效地无限大。
if (Math.abs(period) > (Long.MAX_VALUE >> 1))
period >>= 1;
synchronized(queue) {
if (!thread.newTasksMayBeScheduled)
throw new IllegalStateException("Timer already cancelled.");
synchronized(task.lock) {
if (task.state != TimerTask.VIRGIN)
throw new IllegalStateException(
"Task already scheduled or cancelled");
// 设置下次执行时间
task.nextExecutionTime = time;
task.period = period;
task.state = TimerTask.SCHEDULED;
}
// 加入任务队列
queue.add(task);
// 获取最近要执行的任务,如果是当前任务,唤醒队列
if (queue.getMin() == task)
queue.notify();
}
}
- mainLoop() 主要执行步骤
private void mainLoop() {
while (true) {
try {
TimerTask task;
boolean taskFired;
synchronized(queue) {
// 等待队列变为非空
while (queue.isEmpty() && newTasksMayBeScheduled)
queue.wait();
if (queue.isEmpty())
break; // 队列是空的,将永远保留;死亡
// 队列非空;获取第一个任务去执行
long currentTime, executionTime;
// 获取任务
task = queue.getMin();
synchronized(task.lock) {
// 如果任务状态是取消,无需操作,再次轮询队列
if (task.state == TimerTask.CANCELLED) {
queue.removeMin();
continue;
}
// 当前时间
currentTime = System.currentTimeMillis();
// 下次执行的时间
executionTime = task.nextExecutionTime;
// 如果下次执行时间小于等于当前时间
if (taskFired = (executionTime<=currentTime)) {
// 如果任务是单次的,直接删除
if (task.period == 0) {
queue.removeMin();
task.state = TimerTask.EXECUTED;
} else {
// 重复任务,重新安排,计算下次执行时间
queue.rescheduleMin(
task.period<0 ? currentTime - task.period
: executionTime + task.period);
}
}
}
//任务尚未启动;等待
if (!taskFired)
queue.wait(executionTime - currentTime);
}
if (taskFired)
// 执行任务,注意是单线程运行,不是运行start方法运行的
task.run();
} catch(InterruptedException e) {
}
}
}
总结:Timer是存在缺陷的 单线程 任务阻塞 任务超时
- TaskQueue:小顶堆,存放timeTask
- TimerThread:任务执行线程
- 死循环不断检测是否有任务需要开始执行,有就执行它
- 还是在这个线程执行
- 单线程执行任务,任务有可能相互阻塞
- schedule:任务执行超时会导致后面的任务往后推移,在规定时间内执行的次数会减少
- schedukeAtFixedRate:任务超时可能会导致下一个任务马上执行,不会有中间间隔
- 运行时异常会导致timer线程终止
- 任务调度是基于绝对时间的,对系统时间敏感
② 定时任务线程池
测试代码
/**
* @author cVzhanshi
* @create 2022-11-03 15:05
*/
public class ScheduleThreadPoolTest {
public static void main(String[] args) {
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
for (int i = 0; i < 2; i++){
// 第一个参数:任务
// 第二个参数:第一次执行的时间
// 第三个参数:下次任务间隔的时间
// 第四个参数:时间单位
scheduledThreadPool.scheduleAtFixedRate(new Task("task-" + i ),0,2, TimeUnit.SECONDS);
// 执行单次任务的api
// scheduledThreadPool.schedule(new Task("task-" + i ),0, TimeUnit.SECONDS);
}
}
}
class Task implements Runnable{
private String name;
public Task(String name) {
this.name = name;
}
public void run() {
try {
System.out.println("name="+name+",startTime=" + new Date());
Thread.sleep(3000);
System.out.println("name="+name+",endTime=" + new Date());
//线程池执行
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
ScheduledThreadPoolExecutor
- 使用多线程执行任务,不会相互阻塞
- 如果线程失败,会新建线程执行任务
- 线程抛异常,任务会被丢弃、需要做捕获处理
- DelayedWorkQueue:小顶堆、无界队列
- 在定时线程池中,最大线程数是没有意义的
- 执行时间距离当前时间越近的任务在队列的前面
- 用于添加ScheduleFutureTask(继承Future,实现RunnableScheduledFuture接口)
- 线程池中的线程从DelayQueue中获取ScheduleFutureTask,然后执行任务
- 实现Delayd接口,可以通过getDelay方法来获取延迟时间
- Leader-Follower模式
- 假如说现在有一堆等待执行的任务 (一般是存放在一个队列中排好序) , 而所有的工作线程中只会有一个是leader线程, 其他的线程都是follower线程。只有leader线程能执行任务, 而剩下的follower线程则不会执行任务,它们会处在休眠中的状态。当leader线程 拿到任务后执行任务前,自己会变成follower线程,同时会选出一个新的leader线程,然后才去执行任务。如果此时有下一个任务,就是这个新的leader线程来执行了,并以此往复这个过程。当之前那个执行任务的线程执行完毕再回来时,会判断如果此时已经没任务了,又或者有任务但是有其他的线程作为leader线程,那么自己就休眠了;如果此时有任务但是没有leader线程,那么自己就会重新成为leader线程来执行任务
- 避免没必要的唤醒和阻塞的操作
- 应用场景
- 适用于多个后台线程执行周期性任务,同时为了满足资源管理的需求而需要限制后台线程数量
SingleThreadScheduledExecutor
- 单线程的ScheduledThreadPoolExecutor
- 应用场景:适用于需要单个后台线程执行周期任务,同时需要保证任务顺序执行
③ 定时框架Quartz
3.1 简介
Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,它可以与2EE与|2SE应用程序相结合也可以单独使用。
quartz是开源且具有丰富特性的"任务调度库",能够集成于任何的java应用,小到独立的应用,大至电子商业系统。quartz能够创建亦简单亦复杂的调度,以执行上十、上百,甚至上万的任务。任务job被定义为标准的java组件,能够执行任何你想要实现的功能。quartz调度框架包含许多企业级的特性,如JTA事务、集群的支持。
简而言之,quartz就是基于java实现的任务调度框架,用于执行你想要执行的任何任务。
Quartz所涉及到的设计模式
● Builder模式
● Factory模式
● 组件模式 JobDetail Trigger
● 链式编程
3.2 核心概念和体系结构
- 任务Job
Job就是你想要实现的任务类,每一个Job必须实现org.quartz.job接口,且只需实现接口定义的execute()方法。 - 触发器Trigger
Trigger为你执行任务的触发器,比如你想每天定时3点发送一份统计邮件,Trigger将会设置3点执行该任务。Trigger主要包含两种SimplerTrigger和CronTrigger两种。 - 调度器Scheduler
Scheduler为任务的调度器,它会将任务Job及触发器Trigger整合起来,负责基于Trigger设定的时间来执行Job。
Quartz的体系结构
3.3 常用的组件
以下是Quartz编程API几个重要接口,也是Quartz的重要组件。
- Scheduler - 与调度程序交互的主要API。
- Job - 你想要调度器执行的任务组件需要实现的接口
- JobDetail - 用于定义作业的实例。
- Trigger(即触发器) - 定义执行给定作业的计划的组件。
- JobBuilder - 用于定义/构建 JobDetail 实例,用于定义作业的实例。
- TriggerBuilder - 用于定义/构建触发器实例。
- Scheduler 的生命期,从 SchedulerFactory 创建它时开始,到 Scheduler 调用shutdown() 方法时结束;Scheduler 被创建后,可以增加、删除和列举 Job 和 Trigger,以及执行其它与调度相关的操作(如暂停 Trigger)。但是,Scheduler 只有在调用 start() 方法后,才会真正地触发 trigger(即执行 job)
3.4 入门Demo
- 创建一个spring boot项目
- 导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
- 创建HelloJob
/**
* @author cVzhanshi
* @create 2022-11-02 18:35
*/
public class HelloJob implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println("HelloJob" + new Date());
}
}
- 在main方法中进行配置任务调用
/**
* @author cVzhanshi
* @create 2022-11-02 18:36
*/
public class HelloSchedulerDemo {
public static void main(String[] args) throws SchedulerException {
// 1.调度器(Scheduler),从工厂获取调度实例
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
//2.任务实例(JobDetail)
JobDetail jobDetail = JobBuilder.newJob(HelloJob.class) //加载任务类,与HelloJob完成绑定,要求HelloJob实现Job接口
.withIdentity("job1", "group1") //参数1:任务的名称(唯一实例),参数2:任务组的名称
.build();
//3.触发器(Trigger)
SimpleTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1", "group2") //参数1:触发器的名称(唯一实例),参数2:触发器组的名称
.startNow() // 马上启动触发器
.withSchedule(SimpleScheduleBuilder.simpleSchedule().repeatSecondlyForever(5))// 每5s触发一次,一直执行
.build();
//让调度器关联任务和触发器,保证按照触发器定义的条件执行任务
scheduler.scheduleJob(jobDetail, trigger);
scheduler.start();
}
}
- 执行结果
HelloJobThu Nov 03 16:54:05 CST 2022 //每五秒执行一次
HelloJobThu Nov 03 16:54:10 CST 2022
HelloJobThu Nov 03 16:54:15 CST 2022
3.5 Job 和 JobDetail
public static void main(String[] args) throws SchedulerException {
// 1.调度器(Scheduler),从工厂获取调度实例
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
//2.任务实例(JobDetail)
JobDetail jobDetail = JobBuilder.newJob(HelloJob.class) //加载任务类,与HelloJob完成绑定,要求HelloJob实现Job接口
.withIdentity("job1", "group1") //参数1:任务的名称(唯一实例),参数2:任务组的名称
.build();
//3.触发器(Trigger)
SimpleTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1", "group2") //参数1:触发器的名称(唯一实例),参数2:触发器组的名称
.startNow() // 马上启动触发器
.withSchedule(SimpleScheduleBuilder.simpleSchedule().repeatSecondlyForever(5))
.build();
//让调度器关联任务和触发器,保证按照触发器定义的条件执行任务
scheduler.scheduleJob(jobDetail, trigger);
scheduler.start();
}
通过demo我们知道。我们传给scheduler一个JobDetail实例,因为我们在创建JobDetail时,将要执行的job的类名传给了JobDetail,所以scheduler就知道了要执
行何种类型的job;每次当scheduler执行job时,在调用其execute(…)方法之前会创建该类的一个新的实例;执行完毕,对该实例的引用就被丢弃了,实例会被垃
圾回收;这种执行策略带来的一个后果是,job必须有一个无参的构造函数(当使用默认的JobFactory时);另一个后果是,在job类中,不应该定义有状态的数据
属性,因为在job的多次执行中,这些属性的值不会保留。
那么如何给job实例增加属性或配置呢?如何在job的多次执行中,跟踪job的状态呢?
- JobDataMap
JobDataMap
JobDataMap中可以包含不限量的(序列化的)数据对象,在job实例执行的时候,可以使用其中的数据;JobDataMap是Java Map接口的一个实现,额外增加了一
些便于存取基本类型的数据的方法。
将job加入到scheduler之前,在构建JobDetail时,可以将数据放入JobDataMap,如代码所示:
//创建一个job
JobDetail job = JobBuilder.newJob(HelloJob.class)
.usingJobData("j1", "jv1")
.withIdentity("myjob", "mygroup")
.usingJobData("jobSays", "Hello World!")
.usingJobData("myFloatValue", 3.141f)
.build();
在job的执行过程中,可以从JobDataMap中取出数据,如下示例:
public class HelloJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
Object tv1 = context.getTrigger().getJobDataMap().get("t1");
Object tv2 = context.getTrigger().getJobDataMap().get("t2");
Object jv1 = context.getJobDetail().getJobDataMap().get("j1");
Object jv2 = context.getJobDetail().getJobDataMap().get("j2");
Object sv = null;
try {
sv = context.getScheduler().getContext().get("skey");
} catch (SchedulerException e) {
e.printStackTrace();
}
System.out.println(tv1+":"+tv2);
System.out.println(jv1+":"+jv2);
System.out.println(sv);
System.out.println("hello:"+ LocalDateTime.now());
}
}
如果你在job类中,为JobDataMap中存储的数据的key增加set方法(如在上面示例中,增加setJobSays(String val)方法),那么Quartz的默认JobFactory实现在job被实例化的时候会自动调用这些set方法,这样你就不需要在execute()方法中显式地从map中取数据了。
/**
* @author cVzhanshi
* @create 2022-11-02 18:35
*/
public class HelloJob implements Job {
private String j1;
public void setJ1(String j1) {
this.j1 = j1;
}
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println(j1); // jv1
}
}
JobDetail job = JobBuilder.newJob(HelloJob.class)
.usingJobData("j1", "jv1")
.withIdentity("myjob", "mygroup")
.build();
在Job执行时,JobExecutionContext中的JobDataMap为我们提供了很多的便利。它是JobDetail中的JobDataMap和Trigger中的JobDataMap的并集,但是如果存
在相同的数据,则后者会覆盖前者的值。
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
JobDataMap mergedJobDataMap = jobExecutionContext.getMergedJobDataMap();
System.out.println(mergedJobDataMap.get("j1"));
System.out.println(mergedJobDataMap.get("t1"));
System.out.println(mergedJobDataMap.get("name")); // jobDetail中的数据会被trigger覆盖,如果key相同的话
}
Job实例
- 你可以只创建一个job类,然后创建多个与该job关联的JobDetail实例,每一个实例都有自己的属性集和JobDataMap,最后,将所有的实例都加到scheduler中。
- 比如,你创建了一个实现Job接口的类“SalesReportJob”。该job需要一个参数(通过JobdataMap传入),表示负责该销售报告的销售员的名字。因此,你可以创建该job的多个实例(JobDetail),比如“SalesReportForJoe”、“SalesReportForMike”,将“joe”和“mike”作为JobDataMap的数据传给对应的job实例。
- 当一个trigger被触发时,与之关联的JobDetail实例会被加载,JobDetail引用的job类通过配置在Scheduler上的JobFactory进行初始化。默认的JobFactory实现,仅仅是调用job类的newInstance()方法,然后尝试调用JobDataMap中的key的setter方法。你也可以创建自己的JobFactory实现,比如让你的IOC或DI容器可以创建/初始化job实例。
- 在Quartz的描述语言中,我们将保存后的JobDetail称为“job定义”或者“JobDetail实例”,将一个正在执行的job称为“job实例”或者“job定义的实例”。当我们使用“job”时,一般指代的是job定义,或者JobDetail;当我们提到实现Job接口的类时,通常使用“job类”。
Job状态与并发
- Scheduler每次执行,都会根据JobDetail创建一个新的 Job实例,这样就可以规避并发访文的问题(jobDeatil的实例也是新的),看如下代码示例
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println("JobDetail: " + System.identityHashCode(jobExecutionContext.getJobDetail().hashCode()));
System.out.println("Job: " + System.identityHashCode(jobExecutionContext.getJobInstance().hashCode()));
}
JobDetail: 2046941357
Job: 1895412701
// 每次都不一样
JobDetail: 1157785854
Job: 1341784974
- **Quartz定时任务默认都是并发执行的,不会等待上一次任务执行完毕,只要间隔时间到就会执行,如果定时任执行太长,会长时间占用资源,导致其它任务堵塞。**测试并发执行:
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println("execute:" + new Date());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 任务设置成1s执行一次
// 执行结果 还是每秒执行一次,并没有sleep3s 所以是并发执行
// execute:Thu Nov 03 17:36:55 CST 2022
// execute:Thu Nov 03 17:36:56 CST 2022
// execute:Thu Nov 03 17:36:57 CST 2022
关于job的状态数据(即JobDataMap)和并发性,还有一些地方需要注意。在job类上可以加入一些注解,这些注解会影响job的状态和并发性。
- @DisallowConcurrentExecution:将该注解加到job类上,告诉Quartz不要并发地执行同一个job定义(这里指特定的job类)的多个实例。拿上面的例子
来说,如果“SalesReportJob”类上有该注解,则同一时刻仅允许执行一个“SalesReportForJoe”实例,但可以并发地执行“SalesReportForMike”类的一个实例。
所以该限制是针对JobDetail的,而不是job类的。但是我们认为(在设计Quartz的时候)应该将该注解放在job类上,因为job类的改变经常会导致其行为发生
变化。
代码测试
@DisallowConcurrentExecution
public class HelloJob implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println("execute:" + new Date());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 任务设置成1s执行一次
// 执行结果
// execute:Thu Nov 03 17:36:55 CST 2022
// execute:Thu Nov 03 17:36:58 CST 2022
执行结果是睡眠了3s才执行下一个任务,所以没有并发去执行同一个job定义(这里指特定的job类)的多个实例
- **@PersistJobDataAfterExecution:将该注解加在job类上,告诉Quartz在成功执行了job类的execute方法后(没有发生任何异常),更新JobDetail(对Trigger中的datamap无效)中JobDataMap的数据,使得该job(即JobDetail)在下一次执行的时候,JobDataMap中是更新后的数据,而不是更新前的旧数据。**和 @DisallowConcurrentExecution注解一样,尽管注解是加在job类上的,但其限制作用是针对job实例的,而不是job类的。由job类来承载注解,是因为job类的内容经常会影响其行为状态(比如,job类的execute方法需要显式地“理解”其”状态“)。说明:因为每次执行实例都是一个新的实例,那么实例里面的数据就是新的,加上这个注解上一个实例更新玩会去更新这个数据给下个实例使用
如果你使用了@PersistJobDataAfterExecution注解,我们强烈建议你同时使用@DisallowConcurrentExecution注解,因为当同一个job(JobDetail)的两个实例
被并发执行时,由于竞争,JobDataMap中存储的数据很可能是不确定的。
Job的其它特性
通过JobDetail对象,可以给job实例配置的其它属性有:
- Durability:如果一个job是非持久的,当没有活跃的trigger与之关联的时候,会被自动地从scheduler中删除。也就是说,非持久的job的生命期是由trigger的
存在与否决定的; - RequestsRecovery:如果一个job是可恢复的,并且在其执行的时候,scheduler发生硬关闭(hard shutdown)(比如运行的进程崩溃了,或者关机了),则
当scheduler重新启动的时候,该job会被重新执行。此时,该job的JobExecutionContext.isRecovering() 返回true。
3.6 Trigger
Trigger的公共属性
所有类型的trigger都有TriggerKey这个属性,表示trigger的身份;除此之外,trigger还有很多其它的公共属性。这些属性,在构建trigger的时候可以通
TriggerBuilder设置。
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号。
优先级
如果你的trigger很多(或者Quartz线程池的工作线程太少),Quartz可能没有足够的资源同时触发所有的trigger;这种情况下,你可能希望控制哪些trigger优先使用
Quartz的工作线程,要达到该目的,可以在trigger上设置priority属性。比如,你有N个trigger需要同时触发,但只有Z个工作线程,优先级最高的Z个trigger会被
首先触发。如果没有为trigger设置优先级,trigger使用默认优先级,值为5;priority属性的值可以是任意整数,正数、负数都可以。
- 注意:只有同时触发的trigger之间才会比较优先级。10:59触发的trigger总是在11:00触发的trigger之前执行。
- 注意:如果trigger是可恢复的,在恢复后再调度时,优先级与原trigger是一样的。
错过触发
trigger还有一个重要的属性misfire;如果scheduler关闭了,或者Quartz线程池中没有可用的线程来执行job,此时持久性的trigger就会错过(miss)其触发时间,
即错过触发(misfire)。不同类型的trigger,有不同的misfire机制。它们默认都使用“智能机制(smart policy)”,即根据trigger的类型和配置动态调整行为。当
scheduler启动的时候,查询所有错过触发(misfire)的持久性trigger。然后根据它们各自的misfire机制更新trigger的信息。
判断misfire的条件
- job达到触发的时间没有被执行
- 被执行的延迟时间超过了Quartz配置的misfire Threshole阈值
产生的可能原因
- 当job达到触发时间时,所有线程都被其他job占用,没有可用线程
- 在job需要触发的时间点,scheduler停止 了(可能是意外停止的)
- job使用了@ DisallowConcurrentExecution注解,job不能并发执行,当达到下一个job执行点的时候,.上一 个任务还没有完成
- job指定了过去的开始执行时间,例如当前时间是8点00分00秒,指定开始时间为7点00分00秒
3.7 Spring Boot整合Quartz
- 创建springboot项目
- 导入需要的依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.7</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.cvzhanshi</groupId>
<artifactId>test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>test</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
- 编辑配置文件application.yaml
server:
port: 8889
datasource:
url: jdbc:mysql://localhost:3306/test_quartz?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
- 编辑Quartz的配置文件spring-quartz.properties
#============================================================================
# 配置JobStore
#============================================================================
# JobDataMaps是否都为String类型,默认false
org.quartz.jobStore.useProperties=false
# 表的前缀,默认QRTZ_
org.quartz.jobStore.tablePrefix = QRTZ_
# 是否加入集群
org.quartz.jobStore.isClustered = true
# 调度实例失效的检查时间间隔 ms
org.quartz.jobStore.clusterCheckinInterval = 5000
# 数据保存方式为数据库持久化
org.quartz.jobStore.class=org.springframework.scheduling.quartz.LocalDataSourceJobStore
# 数据库代理类,一般org.quartz.impl.jdbcjobstore.StdJDBCDelegate可以满足大部分数据库
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
#============================================================================
# Scheduler 调度器属性配置
#============================================================================
# 调度标识名 集群中每一个实例都必须使用相同的名称
org.quartz.scheduler.instanceName = ClusterQuartz
# ID设置为自动获取 每一个必须不同
org.quartz.scheduler.instanceId= AUTO
#============================================================================
# 配置ThreadPool
#============================================================================
# 线程池的实现类(一般使用SimpleThreadPool即可满足几乎所有用户的需求)
org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
# 指定线程数,一般设置为1-100直接的整数,根据系统资源配置
org.quartz.threadPool.threadCount = 5
# 设置线程的优先级(可以是Thread.MIN_PRIORITY(即1)和Thread.MAX_PRIORITY(这是10)之间的任何int 。默认值为Thread.NORM_PRIORITY(5)。)
org.quartz.threadPool.threadPriority = 5
- 编辑Job类
/**
* @author cVzhanshi
* @create 2022-11-07 10:20
*/
@PersistJobDataAfterExecution
@DisallowConcurrentExecution
public class QuartzJob extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
try {
Thread.sleep(2000);
System.out.println(context.getScheduler().getSchedulerInstanceId());
System.out.println("taskname=" + context.getJobDetail().getKey().getName());
System.out.println("执行时间:" + new Date());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (SchedulerException e) {
e.printStackTrace();
}
}
}
- 统一使用一个配置好的调度器Scheduler
/**
* @author cVzhanshi
* @create 2022-11-07 10:53
*/
@Configuration
public class SchedulerConfig {
@Autowired
private DataSource dataSource;
@Bean
public Scheduler scheduler() throws IOException {
return schedulerFactoryBean().getScheduler();
}
@Bean
public SchedulerFactoryBean schedulerFactoryBean() throws IOException {
SchedulerFactoryBean factoryBean = new SchedulerFactoryBean();
factoryBean.setSchedulerName("cluster_scheduler");
// 设置数据源
factoryBean.setDataSource(dataSource);
factoryBean.setApplicationContextSchedulerContextKey("application");
// 加载配置文件
factoryBean.setQuartzProperties(quartzProperties());
// 设置线程池
factoryBean.setTaskExecutor(schedulerThreadPool());
// 设置延迟开启任务时间
factoryBean.setStartupDelay(0);
return factoryBean;
}
@Bean
public Properties quartzProperties() throws IOException {
PropertiesFactoryBean factoryBean = new PropertiesFactoryBean();
factoryBean.setLocation(new ClassPathResource("/spring-quartz.properties"));
factoryBean.afterPropertiesSet();
return factoryBean.getObject();
}
@Bean
public Executor schedulerThreadPool(){
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(Runtime.getRuntime().availableProcessors());
executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors());
executor.setQueueCapacity(Runtime.getRuntime().availableProcessors());
return executor;
}
}
- 编写监听器,监听容器加载事件,加载完成就启动任务
/**
* @author cVzhanshi
* @create 2022-11-07 11:04
*/
@Component
// 监听springboot容器是否启动,启动了就执行定时任务
public class StartApplicationListener implements ApplicationListener<ContextRefreshedEvent> {
@Autowired
private Scheduler scheduler;
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
TriggerKey triggerKey = TriggerKey.triggerKey("trigger1", "group1");
try {
/**
* 在调度器中获取指定key的trigger,
*/
Trigger trigger = scheduler.getTrigger(triggerKey);
if(trigger == null){
trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1","group1")
.withSchedule(CronScheduleBuilder.cronSchedule("0/10 * * * * ?"))
.startNow()
.build();
}
JobDetail jobDetail = JobBuilder.newJob(QuartzJob.class).withIdentity("job1", "group1").build();
scheduler.scheduleJob(jobDetail, trigger);
scheduler.start();
} catch (SchedulerException e) {
e.printStackTrace();
}
}
}
- 启动服务
# 输出结果
taskname=job1
执行时间:Mon Nov 07 14:19:02 CST 2022
A029095-NC1667801934353
taskname=job1
执行时间:Mon Nov 07 14:19:12 CST 2022
3.8 Quartz集群
Quartz集群是一个jobdetail分配到一个节点且一直在这个节点不会改变的
demo示例:
- 修改上述整合的代码
/**
* @author cVzhanshi
* @create 2022-11-07 11:04
*/
@Component
// 监听springboot容器是否启动,启动了就执行定时任务
public class StartApplicationListener implements ApplicationListener<ContextRefreshedEvent> {
@Autowired
private Scheduler scheduler;
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
try {
TriggerKey triggerKey = TriggerKey.triggerKey("trigger1", "group1");
/**
* 在调度器中获取指定key的trigger,
*/
Trigger trigger = scheduler.getTrigger(triggerKey);
if(trigger == null){
trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1","group1")
.withSchedule(CronScheduleBuilder.cronSchedule("0/10 * * * * ?"))
.startNow()
.build();
}
JobDetail jobDetail = JobBuilder.newJob(QuartzJob.class).withIdentity("job1", "group1").build();
scheduler.scheduleJob(jobDetail, trigger);
TriggerKey triggerKey2 = TriggerKey.triggerKey("trigger1", "group1");
/**
* 在调度器中获取指定key的trigger,
*/
Trigger trigger2 = scheduler.getTrigger(triggerKey2);
if(trigger2 == null){
trigger2 = TriggerBuilder.newTrigger()
.withIdentity("trigger2","group2")
.withSchedule(CronScheduleBuilder.cronSchedule("0/10 * * * * ?"))
.startNow()
.build();
}
JobDetail jobDetail2 = JobBuilder.newJob(QuartzJob.class).withIdentity("job2", "group2").build();
scheduler.scheduleJob(jobDetail2, trigger2);
scheduler.start();
} catch (SchedulerException e) {
e.printStackTrace();
}
}
}
- 先启动一个节点
# 任务是在同样一个节点执行的
A029095-NC1667802623784
taskname=job1
执行时间:Mon Nov 07 14:30:32 CST 2022
A029095-NC1667802623784
taskname=job2
执行时间:Mon Nov 07 14:30:32 CST 2022
A029095-NC1667802623784
taskname=job1
执行时间:Mon Nov 07 14:30:42 CST 2022
A029095-NC1667802623784
taskname=job2
执行时间:Mon Nov 07 14:30:42 CST 2022
- 换一个端口号在启动一个节点
第一个节点
A029095-NC1667802623784
taskname=job1
执行时间:Mon Nov 07 14:31:42 CST 2022
A029095-NC1667802623784
taskname=job1
执行时间:Mon Nov 07 14:31:52 CST 2022
第二个节点
A029095-NC1667802696272
taskname=job2
执行时间:Mon Nov 07 14:31:42 CST 2022
A029095-NC1667802696272
taskname=job2
执行时间:Mon Nov 07 14:31:52 CST 2022
v