• 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(执行时间会乱)
                //单线程  任务阻塞  任务超时
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    执行源码解析

    // Timer类中的两个常量
    // 任务队列
    private final TaskQueue queue = new TaskQueue();
    // 执行任务的线程
    private final TimerThread thread = new TimerThread(queue);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    1. new Timer();

      // 进入构造器
      public Timer() {
          // 调用了另一个构造器
          this("Timer-" + serialNumber());
      }
      
      public Timer(String name) {
          thread.setName(name);
          // 启动线程执行线程的run方法
          thread.start();
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
    2. 线程的run()方法

      public void run() {
          try {
              // 主要方法 后面再说
              mainLoop();
          } finally {
            ...
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
    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();
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
    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) {
              }
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51

    总结: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();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    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.bootgroupId>
          <artifactId>spring-boot-starter-quartzartifactId>
      dependency>
      
      • 1
      • 2
      • 3
      • 4
    • 创建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());
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
    • 在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();
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
    • 执行结果

      HelloJobThu Nov 03 16:54:05 CST 2022 //每五秒执行一次
      
      HelloJobThu Nov 03 16:54:10 CST 2022
      
      HelloJobThu Nov 03 16:54:15 CST 2022
      
      • 1
      • 2
      • 3
      • 4
      • 5

    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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    通过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();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在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());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    如果你在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();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    在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相同的话
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    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
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
    • **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 
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15

    关于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 
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17

    执行结果是睡眠了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>
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
    • 编辑配置文件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
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
    • 编辑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
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
    • 编辑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();
              }
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
    • 统一使用一个配置好的调度器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;
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
    • 编写监听器,监听容器加载事件,加载完成就启动任务

      /**
       * @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();
              }
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
    • 启动服务

      # 输出结果
      taskname=job1
      执行时间:Mon Nov 07 14:19:02 CST 2022
      A029095-NC1667801934353
      taskname=job1
      执行时间:Mon Nov 07 14:19:12 CST 2022
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6

    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();
              }
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
    • 先启动一个节点

      # 任务是在同样一个节点执行的
      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
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
    • 换一个端口号在启动一个节点

      第一个节点

      A029095-NC1667802623784
      taskname=job1
      执行时间:Mon Nov 07 14:31:42 CST 2022
      A029095-NC1667802623784
      taskname=job1
      执行时间:Mon Nov 07 14:31:52 CST 2022
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6

      第二个节点

      A029095-NC1667802696272
      taskname=job2
      执行时间:Mon Nov 07 14:31:42 CST 2022
      A029095-NC1667802696272
      taskname=job2
      执行时间:Mon Nov 07 14:31:52 CST 2022
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6

    v

  • 相关阅读:
    Openssl数据安全传输平台011:秘钥协商客户端
    java Map集合获取方法
    SQL 优化
    正则表达式常用示例
    分布式搜索引擎Elasticsearch
    Advent of Code --- day 5
    【Java基础】Java容器相关知识小结
    443-C++基础语法(121-130)
    Linux与shell命令行学习
    单视觉L2市场「鲶鱼」来了,掀起数据反哺高阶新打法
  • 原文地址:https://blog.csdn.net/qq_45408390/article/details/127732350