Java定时任务的解决方案(Quartz等)

① 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();
    }
    
  2. 线程的run()方法

    public void run() {
          
          
        try {
          
          
            // 主要方法 后面再说
            mainLoop();
        } finally {
          
          
          ...
        }
    }
    
  3. 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();
        }
    }
    
  4. 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

猜你喜欢

转载自blog.csdn.net/qq_45408390/article/details/127732350
今日推荐