① 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);
  1. new Timer();
// 进入构造器
public Timer() {
    // 调用了另一个构造器
    this("Timer-" + serialNumber());
}

public Timer(String name) {
    thread.setName(name);
    // 启动线程执行线程的run方法
    thread.start();
}
  1. 线程的run()方法
public void run() {
    try {
        // 主要方法 后面再说
        mainLoop();
    } finally {
      ...
    }
}
  1. 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();
    }
}
  1. 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

学习文档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 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