SpringBoot集成Quartz实现分布式任务调度

1,943 阅读7分钟

前言

本篇内容包括

  • SpringBoot整合Quartz
  • Quartz持久化
  • 分布式任务调度

一、介绍

1.1 Quartz集群

​ Quartz集群中每个节点都是一个单独的Quartz应用,它又管理着其他的节点。这个集群需要每个节点单独的启动或停止;和我们的应用服务器集群不同,独立的Quratz节点之间是不需要通信的。不同节点之间是通过数据库表来感知另一个应用。只有使用持久的JobStore才能完成Quartz集群。

07.SpringBoot集成Quartz实现分布式任务调度01.png

1.2 Quartz持久化

Quartz持久化配置提供了两种存储器:

类型优点缺点
RAMJobStore不要外部数据库,配置容易,运行速度快因为调度程序信息是存储在被分配给 JVM 的内存里面,所以,当应用程序停止运行时,所有调度信息将被丢失。另外因为存储到JVM内存里面,所以可以存储多少个 Job 和 Trigger 将会受到限制
JDBC 作业存储支持集群,因为所有的任务信息都会保存到数据库中,可以控制事物,还有就是如果应用服务器关闭或者重启,任务信息都不会丢失,并且可以恢复因服务器关闭或者重启而导致执行失败的任务运行速度的快慢取决与连接数据库的快慢

二、操作步骤

需要提前创建一个SpringBoot项目

2.1 引入依赖包

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>commons-lang</groupId>
    <artifactId>commons-lang</artifactId>
    <version>2.5</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.39</version>
</dependency>
<dependency>
    <groupId>com.mchange</groupId>
    <artifactId>c3p0</artifactId>
    <version>0.9.5.4</version>
</dependency>

2.2 配置文件

通过在application.yml配置文件中对quartz进行相关配置。

server:
  port: 8090

spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/quartz?characterEncoding=utf-8&useSSL=false
    username: root
    password: 123456
  quartz:
    jdbc:
      initialize-schema: never # 是否自动使用 SQL 初始化 Quartz 表结构。always:总是,never:不需要
    job-store-type: jdbc # Job 存储器类型。默认为 memory 表示内存,可选 jdbc 使用数据库。
    properties:
      org:
        quartz:
          scheduler:
            instanceName: QuartzScheduler # 调度标识名 集群中每一个实例都必须使用相同的名称
            instanceId: AUTO # 定时任务的实例编号, 如果手动指定需要保证每个节点的唯一性
          threadPool:
            class: org.quartz.simpl.SimpleThreadPool
            threadCount: 100  # 线程池大小。默认为 10 。
            threadPriority: 5 # 线程优先级
          jobStore:
            misfireThreshold: 120000

说明:application.yml文件中的配置相当与Quartz中quartz.properties配置文件

2.3 创建数据库表

Quartz持久化过程创建数据库表方式

第一种

将yml文件中配置项initialize-schema在第一次执行时指定为always,会自动在数据库中生成表信息,当表创建完成后在将参数改成never

initialize-schema: always

第二种

在官网中下载Quartz对应版本的安装包在docs\dbTables目录下找到tables_mysql_innodb.sql文件在数据库中执行SQL。

官网下载地址:www.quartz-scheduler.org/downloads/

数据库表结构说明
表名说明
qrtz_blob_triggers以Blob 类型存储的触发器
qrtz_calendars存放日历信息, quartz可配置一个日历来指定一个时间范围
qrtz_cron_triggers存放cron类型的触发器
qrtz_fired_triggers存放已触发的触发器
qrtz_job_details存放一个jobDetail信息
qrtz_job_listenersjob监听器
qrtz_locks存储程序的悲观锁的信息(假如使用了悲观锁)
qrtz_paused_trigger_graps存放暂停掉的触发器
qrtz_scheduler_state调度器状态
qrtz_simple_triggers简单触发器的信息
qrtz_trigger_listeners触发器监听器
qrtz_triggers触发器的基本信息

2.4 定义Job

  • QuartzJobFirst
import com.dataojo.quartz.util.SchedulerUtils;
import org.quartz.*;
import org.springframework.scheduling.quartz.QuartzJobBean;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;

/**
 * @author lilinchao
 * @date 2021/9/2
 * @description 1.0
 **/
@PersistJobDataAfterExecution
@DisallowConcurrentExecution
public class QuartzJobFirst extends QuartzJobBean {
    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext){
        SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        List<JobExecutionContext> executionContexts;
        try {
            executionContexts = SchedulerUtils.getScheduler().getCurrentlyExecutingJobs();
        } catch (SchedulerException e) {
            e.printStackTrace();
            return;
        }
        for (JobExecutionContext executionContext : executionContexts){
            JobKey jobKey = executionContext.getJobDetail().getKey();
            Date fireTime = executionContext.getFireTime();
            System.out.println(jobKey+",对应的执行时间是"+sf.format(fireTime));
        }
    }
}
  • QuartzJobTwo
import org.quartz.*;
import org.springframework.scheduling.quartz.QuartzJobBean;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @author lilinchao
 * @date 2021/9/2
 * @description 1.0
 **/
@PersistJobDataAfterExecution
@DisallowConcurrentExecution
public class QuartzJobTwo extends QuartzJobBean {
    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext){
        SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println("QuartzJobTwo执行时间是"+sf.format(new Date()));
    }
}

说明

  • QuartzJobBean

    QuartzJobBean已经实现了job接口,并重写了接口中的execute()方法,在SpringBoot集成时候直接继承QuartzJobBean即可。执行逻辑在executeInternal()方法中。

  • @PersistJobDataAfterExecution

    告诉Quartz在成功执行了Job实现类的execute方法后(没有发生任何异常),更新JobDetail中JobDataMap的数据,使得该JobDetail实例在下一次执行的时候,JobDataMap中是更新后的数据,而不是更新前的旧数据。

  • @DisallowConcurrentExecution

    告诉Quartz不要并发地执行同一个JobDetail实例。

总结

  • 当某一个JobDetail实例到点运行之后,在其运行结束之前,不会再发起一次该JobDetail实例的调用,即使设置的该JobDetail实例的定时执行时间到了。

  • JobDetail实例之间互不影响。

2.5 实现ApplicationListener 完成动态调度

​ 根据ApplicationListener的原理,其onApplicationEvent(ContextRefreshedEvent event) 方法会在初始化所有的bean之后被调用,因此我们可以在这里进行scheduler的创建、启动,以及注册trigger和job。

import com.dataojo.quartz.job.QuartzJobFirst;
import com.dataojo.quartz.job.QuartzJobTwo;
import com.dataojo.quartz.util.SchedulerUtils;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;

/**
 * @author lilinchao
 * @date 2021/9/2
 * @description 1.0
 **/
@Component
public class StartApplicationListener implements ApplicationListener<ContextRefreshedEvent> {
    @Override
    public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
        System.out.println("-------执行StartApplicationListener--------");
        SchedulerUtils.scheduleCronJob(QuartzJobFirst.class, "*/5 * * * * ?");
        SchedulerUtils.scheduleCronJob(QuartzJobTwo.class, "*/10 * * * * ?");
    }
}

2.6 工具类

  • SchedulerUtils
import org.apache.commons.lang.time.DateUtils;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.text.ParseException;
import java.util.Date;

/**
 * @author lilinchao
 * @date 2021/9/3
 * @description 1.0
 */
public class SchedulerUtils {

    private static final Logger LOGGER = LoggerFactory.getLogger(SchedulerUtils.class);

    private static Scheduler scheduler;

    private static String jobGroup = "group";

    static {
        if (SpringContextUtils.getApplicationContext() != null) {
            scheduler = SpringContextUtils.getBean(Scheduler.class);
        } else {
            SchedulerFactory schedulerFactory = new StdSchedulerFactory();
            try {
                scheduler = schedulerFactory.getScheduler();
                scheduler.start();
            } catch (SchedulerException e) {
                throw new RuntimeException(e);
            }
        }
    }

    private SchedulerUtils() {

    }

    public static Scheduler getScheduler() {
        return scheduler;
    }

    public static void scheduleCronJob(Class<? extends Job> jobClass, String cronExpression) {
        scheduleCronJob(jobClass, jobClass.getSimpleName(), cronExpression);
    }

    public static void scheduleCronJob(Class<? extends Job> jobClass, Object name, String cronExpression) {
        scheduleCronJob(jobClass, name, jobGroup, cronExpression);
    }

    public static void scheduleCronJob(Class<? extends Job> jobClass, Object name, String group, String cronExpression) {
        scheduleCronJob(jobClass, name, group, cronExpression, null);
    }

    public static void scheduleCronJob(Class<? extends Job> jobClass, Object name, String group, String cronExpression, JobDataMap jobDataMap) {
        scheduleCronJob(jobClass, name, group, cronExpression, jobDataMap, null, null);
    }

    public static void scheduleCronJob(Class<? extends Job> jobClass, Object name, String group, String cronExpression, JobDataMap jobDataMap, Date startDate, Date endDate) {
        try {
            JobKey jobKey = new JobKey(String.valueOf(name), group);
            if (!scheduler.checkExists(jobKey)) {
                JobBuilder jobBuilder = JobBuilder.newJob(jobClass);
                jobBuilder.withIdentity(jobKey);
                if (jobDataMap != null && !jobDataMap.isEmpty()) {
                    jobBuilder.setJobData(jobDataMap);
                }
                JobDetail jobDetail = jobBuilder.build();
                CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression).withMisfireHandlingInstructionDoNothing();
                TriggerBuilder<CronTrigger> triggerBuilder = TriggerBuilder.newTrigger().withSchedule(cronScheduleBuilder);
                if (startDate != null) {
                    triggerBuilder.startAt(startDate);
                } else {
                    triggerBuilder.startNow();
                }
                if (endDate != null) {
                    triggerBuilder.endAt(endDate);
                }
                CronTrigger trigger = triggerBuilder.build();
                scheduler.scheduleJob(jobDetail, trigger);
            }
        } catch (Exception e) {
            LOGGER.error("Submit job error, name=" + name + " and group=" + group, e);
        }
    }

    /**
     * 默认立即执行且只执行一次
     * @param jobClass
     */
    public static void scheduleSimpleJob(Class<? extends Job> jobClass) {
        scheduleSimpleJob(jobClass, jobClass.getSimpleName());
    }

    public static void scheduleSimpleJob(Class<? extends Job> jobClass, Object name) {
        scheduleSimpleJob(jobClass, name, 0, 0);
    }

    /**
     * @param jobClass
     * @param name
     * @param intervalInMilliseconds 执行间隔
     * @param repeatCount 重复次数,小于0的时候重复执行
     */
    public static void scheduleSimpleJob(Class<? extends Job> jobClass, Object name, long intervalInMilliseconds, int repeatCount) {
        scheduleSimpleJob(jobClass, name, jobGroup, intervalInMilliseconds, repeatCount);
    }

    public static void scheduleSimpleJob(Class<? extends Job> jobClass, Object name, String group, long intervalInMilliseconds, int repeatCount) {
        scheduleSimpleJob(jobClass, name, group, intervalInMilliseconds, repeatCount, null);
    }

    public static void scheduleSimpleJob(Class<? extends Job> jobClass, Object name, String group, long intervalInMilliseconds, int repeatCount, JobDataMap jobDataMap) {
        scheduleSimpleJob(jobClass, name, group, intervalInMilliseconds, repeatCount, jobDataMap, null, null);
    }

    public static void scheduleSimpleJob(Class<? extends Job> jobClass, Object name, String group, long intervalInMilliseconds, int repeatCount, JobDataMap jobDataMap, Date startDate, Date endDate) {
        try {
            JobKey jobKey = new JobKey(String.valueOf(name), group);
            if (!scheduler.checkExists(jobKey)) {
                JobBuilder jobBuilder = JobBuilder.newJob(jobClass);
                jobBuilder.withIdentity(jobKey);
                if (jobDataMap != null && !jobDataMap.isEmpty()) {
                    jobBuilder.setJobData(jobDataMap);
                }
                JobDetail jobDetail = jobBuilder.build();
                SimpleScheduleBuilder simpleScheduleBuilder = SimpleScheduleBuilder.simpleSchedule();
                simpleScheduleBuilder.withIntervalInMilliseconds(intervalInMilliseconds);
                if (repeatCount >= 0) {
                    simpleScheduleBuilder.withRepeatCount(repeatCount);
                } else {
                    simpleScheduleBuilder.repeatForever();
                }
                TriggerBuilder<SimpleTrigger> triggerBuilder = TriggerBuilder.newTrigger().withSchedule(simpleScheduleBuilder);
                if (startDate != null) {
                    triggerBuilder.startAt(startDate);
                } else {
                    triggerBuilder.startNow();
                }
                if (endDate != null) {
                    triggerBuilder.endAt(endDate);
                }
                SimpleTrigger trigger = triggerBuilder.build();
                scheduler.scheduleJob(jobDetail, trigger);
            }
        } catch (Exception e) {
            LOGGER.error("Submit job error, name=" + name + " and group=" + group, e);
        }
    }

    public static void interrupt(Object name, String group) {
        JobKey jobKey = new JobKey(String.valueOf(name), group);
        try {
            if (scheduler.checkExists(jobKey)) {
                scheduler.interrupt(jobKey);
            }
        } catch (SchedulerException e) {
            LOGGER.warn("Interrupt job error, name=" + name + " and group=" + group, e);
        }
    }

    public static void deleteJob(Object name, String group) {
        JobKey jobKey = new JobKey(String.valueOf(name), group);
        try {
            if (scheduler.checkExists(jobKey)) {
                scheduler.deleteJob(jobKey);
            }
        } catch (SchedulerException e) {
            LOGGER.warn("Delete job error, name=" + name + " and group=" + group, e);
        }
    }

    public static boolean checkExists(Object name, String group) {
        JobKey jobKey = new JobKey(String.valueOf(name), group);
        try {
            return scheduler.checkExists(jobKey);
        } catch (SchedulerException e) {
            LOGGER.warn("CheckExists job error, name=" + name + " and group=" + group, e);
        }
        return false;
    }

    public static Date getNeedFireTime(String cron, Date startDate) {
        Date nextFireTime1 = getNextFireTime(cron, startDate);
        Date nextFireTime2 = getNextFireTime(cron, nextFireTime1);
        int intervals = (int) (nextFireTime2.getTime() - nextFireTime1.getTime());
        Date cal1 = DateUtils.addMilliseconds(nextFireTime1, - intervals);
        Date cal2 = getNextFireTime(cron, cal1);
        Date cal3 = getNextFireTime(cron, cal2);
        while (!cal3.equals(nextFireTime1)) {
            cal1 = DateUtils.addMilliseconds(cal1, - intervals);
            cal2 = getNextFireTime(cron, cal1);
            cal3 = getNextFireTime(cron, cal2);
            if (cal3.before(nextFireTime1)) {
                intervals = -1000;
            }
        }
        return cal2;
    }

    public static Date getNextFireTime(String cron, Date startDate) {
        return getCronExpression(cron).getTimeAfter(startDate);
    }

    private static CronExpression getCronExpression(String cron) {
        try {
            return new CronExpression(cron);
        } catch (ParseException e) {
            throw new IllegalArgumentException(e);
        }
    }

}
  • SpringContextUtils

    实现ApplicationContextAware的工具类,可以通过其它类引用它以操作spring容器及其中的Bean实例。

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

/**
 * @author lilinchao
 * @date 2021/9/3
 * @description 1.0
 */
@Component
public class SpringContextUtils implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringContextUtils.applicationContext = applicationContext;
    }

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    public static Object getBean(String beanName) throws BeansException {
        return applicationContext.getBean(beanName);
    }

    public static <T> T getBean(String beanName, Class<T> clazz) throws BeansException {
        return applicationContext.getBean(beanName, clazz);
    }

    public static <T> T getBean(Class<T> clazz) throws BeansException {
        return applicationContext.getBean(clazz);
    }

    public static Object getBean(String beanName, Object... args) throws BeansException {
        return applicationContext.getBean(beanName, args);
    }

    public static <T> T getBean(Class<T> clazz, Object... args) throws BeansException {
        return applicationContext.getBean(clazz, args);
    }
}

说明

​ Spring容器会检测容器中的所有Bean,如果发现某个Bean实现了ApplicationContextAware接口,Spring容器会在创建该Bean之后,自动调用该Bean的setApplicationContextAware()方法,调用该方法时,会将容器本身作为参数传给该方法——该方法中的实现部分将Spring传入的参数(容器本身)赋给该类对象的applicationContext实例变量,因此接下来可以通过该applicationContext实例变量来访问容器本身。

2.7 运行结果

07.SpringBoot集成Quartz实现分布式任务调度02.jpg

三、项目目录结构

07.SpringBoot集成Quartz实现分布式任务调度03.jpg

附参考文章链接:

www.jianshu.com/p/ab438d944…

www.cnblogs.com/summerday15…

gitee.com/tqbx/spring…