① JDK定时器:Timer
理论基础:时间轮算法
链表或者数组实现时间轮:while-true-sleep
遍历数组,每个下标放置一个链表,链表节点放置任务,遍历到了就取出执行
缺陷:假设数组长度12代表小时,那么我想13点执行,就不太方便
round型时间轮:任务上记录一个round,遍历到了就将round减一,为0时取出执行
需要遍历所有的任务,效率较低
分层时间轮:使用多个不同维度的轮
天轮:记录几点执行
月轮:记录记号执行
月轮遍历到了,将任务去除放到天轮里面,就可以实现几号几点执行
/*** @author cVzhanshi* @create -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:小顶堆,存放timeTaskTimerThread:任务执行线程 死循环不断检测是否有任务需要开始执行,有就执行它还是在这个线程执行 单线程执行任务,任务有可能相互阻塞 schedule:任务执行超时会导致后面的任务往后推移,在规定时间内执行的次数会减少schedukeAtFixedRate:任务超时可能会导致下一个任务马上执行,不会有中间间隔 运行时异常会导致timer线程终止任务调度是基于绝对时间的,对系统时间敏感
② 定时任务线程池
测试代码
/*** @author cVzhanshi* @create -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();}}}
使用多线程执行任务,不会相互阻塞如果线程失败,会新建线程执行任务 线程抛异常,任务会被丢弃、需要做捕获处理 DelayedWorkQueue:小顶堆、无界队列 在定时线程池中,最大线程数是没有意义的执行时间距离当前时间越近的任务在队列的前面用于添加ScheduleFutureTask(继承Future,实现RunnableScheduledFuture接口)线程池中的线程从DelayQueue中获取ScheduleFutureTask,然后执行任务实现Delayd接口,可以通过getDelay方法来获取延迟时间 Leader-Follower模式 假如说现在有一堆等待执行的任务 (一般是存放在一个队列中排好序) , 而所有的工作线程中只会有一个是leader线程, 其他的线程都是follower线程。只有leader线程能执行任务, 而剩下的follower线程则不会执行任务,它们会处在休眠中的状态。当leader线程 拿到任务后执行任务前,自己会变成follower线程,同时会选出一个新的leader线程,然后才去执行任务。如果此时有下一个任务,就是这个新的leader线程来执行了,并以此往复这个过程。当之前那个执行任务的线程执行完毕再回来时,会判断如果此时已经没任务了,又或者有任务但是有其他的线程作为leader线程,那么自己就休眠了;如果此时有任务但是没有leader线程,那么自己就会重新成为leader线程来执行任务避免没必要的唤醒和阻塞的操作 应用场景 适用于多个后台线程执行周期性任务,同时为了满足资源管理的需求而需要限制后台线程数量ScheduledThreadPoolExecutor
单线程的ScheduledThreadPoolExecutor应用场景:适用于需要单个后台线程执行周期任务,同时需要保证任务顺序执行SingleThreadScheduledExecutor
③ 定时框架Quartz
学习文档1
学习文档2
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 -11-02 18:35*/public class HelloJob implements Job {@Overridepublic void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {System.out.println("HelloJob" + new Date());}}
在main方法中进行配置任务调用
/*** @author cVzhanshi* @create -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 //每五秒执行一次HelloJobThu Nov 03 16:54:10 CST HelloJobThu Nov 03 16:54:15 CST
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,如代码所示:
//创建一个jobJobDetail 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 {@Overridepublic 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 -11-02 18:35*/public class HelloJob implements Job {private String j1;public void setJ1(String j1) {this.j1 = j1;}@Overridepublic 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的并集,但是如果存
在相同的数据,则后者会覆盖前者的值。
@Overridepublic 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的实例也是新的),看如下代码示例
@Overridepublic 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: 2046941357Job: 1895412701// 每次都不一样JobDetail: 1157785854Job: 1341784974
**Quartz定时任务默认都是并发执行的,不会等待上一次任务执行完毕,只要间隔时间到就会执行,如果定时任执行太长,会长时间占用资源,导致其它任务堵塞。**测试并发执行:
@Overridepublic 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 // execute:Thu Nov 03 17:36:56 CST // execute:Thu Nov 03 17:36:57 CST
关于job的状态数据(即JobDataMap)和并发性,还有一些地方需要注意。在job类上可以加入一些注解,这些注解会影响job的状态和并发性。
@DisallowConcurrentExecution:将该注解加到job类上,告诉Quartz不要并发地执行同一个job定义(这里指特定的job类)的多个实例。拿上面的例子
来说,如果“SalesReportJob”类上有该注解,则同一时刻仅允许执行一个“SalesReportForJoe”实例,但可以并发地执行“SalesReportForMike”类的一个实例。
所以该限制是针对JobDetail的,而不是job类的。但是我们认为(在设计Quartz的时候)应该将该注解放在job类上,因为job类的改变经常会导致其行为发生
变化。
代码测试
@DisallowConcurrentExecutionpublic class HelloJob implements Job {@Overridepublic 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 // execute:Thu Nov 03 17:36:58 CST
执行结果是睡眠了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="/POM/4.0.0" xmlns:xsi="/2001/XMLSchema-instance"xsi:schemaLocation="/POM/4.0.0 /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: 8889datasource:url: jdbc:mysql://localhost:3306/test_quartz?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTCusername: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Driver
编辑Quartz的配置文件spring-quartz.properties
#============================================================================# 配置JobStore#============================================================================# JobDataMaps是否都为String类型,默认falseorg.quartz.jobStore.useProperties=false# 表的前缀,默认QRTZ_org.quartz.jobStore.tablePrefix = QRTZ_# 是否加入集群org.quartz.jobStore.isClustered = true# 调度实例失效的检查时间间隔 msorg.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 -11-07 10:20*/@PersistJobDataAfterExecution@DisallowConcurrentExecutionpublic class QuartzJob extends QuartzJobBean {@Overrideprotected 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 -11-07 10:53*/@Configurationpublic class SchedulerConfig {@Autowiredprivate DataSource dataSource;@Beanpublic Scheduler scheduler() throws IOException {return schedulerFactoryBean().getScheduler();}@Beanpublic 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;}@Beanpublic Properties quartzProperties() throws IOException {PropertiesFactoryBean factoryBean = new PropertiesFactoryBean();factoryBean.setLocation(new ClassPathResource("/spring-quartz.properties"));factoryBean.afterPropertiesSet();return factoryBean.getObject();}@Beanpublic 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 -11-07 11:04*/@Component// 监听springboot容器是否启动,启动了就执行定时任务public class StartApplicationListener implements ApplicationListener<ContextRefreshedEvent> {@Autowiredprivate Scheduler scheduler;@Overridepublic 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 A029095-NC1667801934353taskname=job1执行时间:Mon Nov 07 14:19:12 CST
3.8 Quartz集群
Quartz集群是一个jobdetail分配到一个节点且一直在这个节点不会改变的
demo示例:
修改上述整合的代码
/*** @author cVzhanshi* @create -11-07 11:04*/@Component// 监听springboot容器是否启动,启动了就执行定时任务public class StartApplicationListener implements ApplicationListener<ContextRefreshedEvent> {@Autowiredprivate Scheduler scheduler;@Overridepublic 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-NC1667802623784taskname=job1执行时间:Mon Nov 07 14:30:32 CST A029095-NC1667802623784taskname=job2执行时间:Mon Nov 07 14:30:32 CST A029095-NC1667802623784taskname=job1执行时间:Mon Nov 07 14:30:42 CST A029095-NC1667802623784taskname=job2执行时间:Mon Nov 07 14:30:42 CST
换一个端口号在启动一个节点
第一个节点
A029095-NC1667802623784taskname=job1执行时间:Mon Nov 07 14:31:42 CST A029095-NC1667802623784taskname=job1执行时间:Mon Nov 07 14:31:52 CST
第二个节点
A029095-NC1667802696272taskname=job2执行时间:Mon Nov 07 14:31:42 CST A029095-NC1667802696272taskname=job2执行时间:Mon Nov 07 14:31:52 CST
v