一、Quartz入门

发布时间 2023-09-03 20:59:52作者: Stitches

参考:

https://juejin.cn/post/7216679822097252411?searchId=20230726145213061AD6F989D36601FB8B

https://www.jianshu.com/p/b94ebb8780fa

https://www.cnblogs.com/summerday152/p/14193968.htm

Quartz 是什么?

Quartz 是一款开源的任务调度框架,可以用于创建简单或复杂的任务调度,以执行数十、数百甚至上万个任务,这些任务被定义为标准 Java 组件,这些组件可以执行你想让他做的任何事情。Quartz 调度程序包括许多企业级特性,例如支持 JTA 事务(Java Transaction API,简写 JTA) 和集群。

我们知道在 SpringBoot 中有 @Schedule 注解可以用来实现对定时任务的增删改查,但是在某些场景下我们需要动态地修改定时任务,此时 @Schedule 注解很明显无法满足需求。


Quartz 的结构和特性

Quartz 的特性

  1. Quartz 可以嵌入到另一个独立的应用程序中运行;
  2. Quartz 可以在应用程序服务器中实例化并且参与分布式事务;
  3. Quartz 可以作为一个独立程序运行(比如说在 JVM中),我们同时可以通过 RMI(Remote Method Invocation)远程方法调用来使用它;
  4. Quartz 可以实例化为一个独立程序集群(具有负载均衡和容错性)。

Quartz 的组成

  • 触发器(Trigger):当一个触发器触发时,Job 就会被调度执行,触发器就是用来定义何时触发,主要有四种 Trigger:SimpleTriggerCronTriggerDataIntervalTriggerNthIncludedTrigger
  • 调度器(Scheduler):用于调度任务的执行,Scheduler 由工厂创建,主要包括 DirectSchedulerFactoryStdSchedulerFactory两种工厂,创建出的 Scheduler 主要包括 RemoteMBeanSchedulerRemoteSchedulerStdScheduler 三种。
  • 任务(Job):所有任务实例需要实现的接口,任务分为有状态任务和无状态任务,分别是 StateLessJobStateFullJob
  • 任务信息(JobDetail):所有任务的具体信息,例如任务携带的参数、任务的名称、任务组名称;

Quartz 任务执行过程

  • 首先任务类都实现了 Job 接口;
  • 当触发器触发时调度器 Schduler 就会通知零个或多个实现了 JobListener 或者 TriggerListener 接口的对象;
  • 当任务执行完成,会返回一个状态码 JobCompletionCode,我们可以根据状态码确定任务是否执行成功以及后续操作的执行;
  • 任务持久化,Quartz 的 JobStore 接口是任务存储接口,该接口的实现 JDBCJobStore 可以将 JobTrigger 持久化到数据库;接口实现RAMJobStore 可以将 JobTrigger 存储到内存中;

Quartz 简单用例

入门程序

public class QuartzTest {
    public static void main(String[] args) {
        try {
            // 获取Schduler调度器
            Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
            scheduler.start();

            // 任务开始,定义一个Job并绑定到 HelloJob这个任务类上
            // 名字为 job1,组为 group1
            JobDetail job = JobBuilder.newJob(HelloJob.class).withIdentity("job1", "group1").build();

            // 触发任务让任务执行,每隔5秒重复执行一次
            Trigger trigger = TriggerBuilder.newTrigger().withIdentity("trigger1", "group1").startNow()
                    .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(5).repeatForever()).build();

            // 告知Quartz使用Trigger去调度这个Job
            scheduler.scheduleJob(job, trigger);
            // 在线程池关闭之前,有充分的时间去执行 Job
            Thread.sleep(30000);
            scheduler.shutdown();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Quartz 调度任务执行步骤如下:

  • 创建调度器 Scheduler,可通过工厂模式创建默认调度器 QuartzScheduler,该调度器是 Quartz 的核心类,它实现了绑定 JobDetailTrigger,定时调度任务处理;
  • 创建触发器 Trigger,触发器指明了该任务何时会被触发,当任务触发后,调用执行任务的 execute() 方法;

Quartz 配置信息

Quartz 默认会加载 org.quartz 包下的 quartz.properties 配置文件,用户可以自定义 quartz.properties 来替代官方包下的文件,此时优先加载用户自定义的,相关配置信息如下:

# 用于区分同一系统中多个不同的用例
# 如果使用了集群功能,就必须对每个实例使用相同的名称,这些实例逻辑上是同一个 Scheduler
org.quartz.scheduler.instanceName: DefaultQuartzScheduler
# 可以是任意字符串,但如果是集群,scheduler实例的值必须唯一,可以使用AUTO自动生成。
org.quartz.scheduler.instanceId = AUTO
org.quartz.scheduler.rmi.export: false
org.quartz.scheduler.rmi.proxy: false

# 默认false,若是在执行Job之前Quartz开启UserTransaction,此属性应该为true。
# Job执行完毕,JobDataMap更新完(如果是StatefulJob)事务就会提交。默认值是false,可以在job类上使用@ExecuteInJTATransaction 注解,以便在各自的job上决定是否开启JTA事务。
org.quartz.scheduler.wrapJobExecutionInUserTransaction: false

# 一个scheduler节点允许接收的trigger的最大数,默认是1,这个值越大,定时任务执行的越多,但代价是集群节点之间的不均衡。
org.quartz.scheduler.batchTriggerAcquisitionMaxCount=1

# 线程池的实例类,一般为 SimpleThreadPool
org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool
# 线程池数量
org.quartz.threadPool.threadCount: 10
# 线程优先级
org.quartz.threadPool.threadPriority: 5
# 加载任务代码的ClassLoader是否从外部继承
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true

# 最大能够忍受的触发超时时间
org.quartz.jobStore.misfireThreshold: 60000
# 作业存储在内存中,重启会丢失
org.quartz.jobStore.class: org.quartz.simpl.RAMJobStore

剩余详细配置信息可参考: http://www.quartz-scheduler.org/documentation/quartz-2.3.0/configuration/

Quartz 数据库表信息存储

Quartz 的数据库建表语句如下:

DROP TABLE  QRTZ_FIRED_TRIGGERS;
DROP TABLE QRTZ_PAUSED_TRIGGER_GRPS;
DROP TABLE QRTZ_SCHEDULER_STATE;
DROP TABLE QRTZ_LOCKS;
DROP TABLE QRTZ_SIMPLE_TRIGGERS;
DROP TABLE QRTZ_SIMPROP_TRIGGERS;
DROP TABLE QRTZ_CRON_TRIGGERS;
DROP TABLE QRTZ_BLOB_TRIGGERS;
DROP TABLE QRTZ_TRIGGERS;
DROP TABLE QRTZ_JOB_DETAILS;
DROP TABLE QRTZ_CALENDARS;

CREATE TABLE `qrtz_job_details` (
  `SCHED_NAME` varchar(120) COLLATE utf8_bin NOT NULL COMMENT '调度器名称,集群环境中使用必须用同一个名称下的 scheduler,默认使用 QuartzScheduler',
  `JOB_NAME` varchar(200) COLLATE utf8_bin NOT NULL COMMENT '集群中任务的名字',
  `JOB_GROUP` varchar(200) COLLATE utf8_bin NOT NULL COMMENT '集群中任务所属组的名字',
  `DESCRIPTION` varchar(250) COLLATE utf8_bin DEFAULT NULL COMMENT '描述',
  `JOB_CLASS_NAME` varchar(250) COLLATE utf8_bin NOT NULL COMMENT '集群中个note job实现类的完全包名,quartz就是根据这个路径到classpath找到该job类',
  `IS_DURABLE` varchar(1) COLLATE utf8_bin NOT NULL COMMENT '是否持久化,设置为1把任务持久化到数据库',
  `IS_NONCONCURRENT` varchar(1) COLLATE utf8_bin NOT NULL COMMENT '是否并行,该属性可以通过注解配置',
  `IS_UPDATE_DATA` varchar(1) COLLATE utf8_bin NOT NULL,
  `REQUESTS_RECOVERY` varchar(1) COLLATE utf8_bin NOT NULL COMMENT '当一个scheduler失败后,其它实例可以发现那些执行失败的任务,若为1表示其它实例可以执行失败的任务,否则只能等到下一次执行',
  `JOB_DATA` blob COMMENT '存储持久化的任务对象',
  PRIMARY KEY (`SCHED_NAME`,`JOB_NAME`,`JOB_GROUP`)
) ENGINE = InnoDB DEFAULT CHARSET=utf8 COLLATE =utf8_bin;


CREATE TABLE `qrtz_calendars` (
  `SCHED_NAME` varchar(120) COLLATE utf8_bin NOT NULL,
  `CALENDAR_NAME` varchar(200) COLLATE utf8_bin NOT NULL,
  `CALENDAR` blob NOT NULL,
  PRIMARY KEY (`SCHED_NAME`,`CALENDAR_NAME`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

CREATE TABLE `qrtz_triggers` (
  `SCHED_NAME` varchar(120) COLLATE utf8_bin NOT NULL COMMENT '调度器名称,同上',
  `TRIGGER_NAME` varchar(200) COLLATE utf8_bin NOT NULL COMMENT '触发器名字',
  `TRIGGER_GROUP` varchar(200) COLLATE utf8_bin NOT NULL COMMENT '触发器所属组的名字',
  `JOB_NAME` varchar(200) COLLATE utf8_bin NOT NULL COMMENT '任务名',
  `JOB_GROUP` varchar(200) COLLATE utf8_bin NOT NULL COMMENT '任务组',
  `DESCRIPTION` varchar(250) COLLATE utf8_bin DEFAULT NULL,
  `NEXT_FIRE_TIME` bigint(13) DEFAULT NULL COMMENT '下一次触发时间',
  `PREV_FIRE_TIME` bigint(13) DEFAULT NULL COMMENT '上一次触发时间',
  `PRIORITY` int(11) DEFAULT NULL COMMENT '线程优先级',
  `TRIGGER_STATE` varchar(16) COLLATE utf8_bin NOT NULL COMMENT '当前trigger状态,设置为ACQUIRED,如果设置为WAITING,则job不会触发',
  `TRIGGER_TYPE` varchar(8) COLLATE utf8_bin NOT NULL COMMENT '当前触发器的类型',
  `START_TIME` bigint(13) NOT NULL COMMENT '开始时间',
  `END_TIME` bigint(13) DEFAULT NULL COMMENT '结束时间',
  `CALENDAR_NAME` varchar(200) COLLATE utf8_bin DEFAULT NULL,
  `MISFIRE_INSTR` smallint(2) DEFAULT NULL COMMENT 'misfire处理规则,【1:以当前时间为触发频率立刻触发一次,然后按照Cron频率依次执行】,
   【2:不触发立即执行,等待下次Cron触发频率到达时刻开始按照Cron频率依次执行】,
   【-1:以错过的第一个频率时间立刻开始执行,重做错过的所有频率周期后,当下一次触发频率发生时间大于当前时间后,再按照正常的Cron频率依次执行】',
  `JOB_DATA` blob,
  PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`),
  KEY `SCHED_NAME` (`SCHED_NAME`,`JOB_NAME`,`JOB_GROUP`),
  CONSTRAINT `qrtz_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`) REFERENCES `qrtz_job_details` (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;


CREATE TABLE `qrtz_blob_triggers` (
  `SCHED_NAME` varchar(120) COLLATE utf8_bin NOT NULL,
  `TRIGGER_NAME` varchar(200) COLLATE utf8_bin NOT NULL,
  `TRIGGER_GROUP` varchar(200) COLLATE utf8_bin NOT NULL,
  `BLOB_DATA` blob,
  PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`),
  CONSTRAINT `qrtz_blob_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `qrtz_triggers` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;


CREATE TABLE `qrtz_cron_triggers` (
  `SCHED_NAME` varchar(120) COLLATE utf8_bin NOT NULL COMMENT '集群名',
  `TRIGGER_NAME` varchar(200) COLLATE utf8_bin NOT NULL COMMENT '调度器名',
  `TRIGGER_GROUP` varchar(200) COLLATE utf8_bin NOT NULL COMMENT '调度器所在组',
  `CRON_EXPRESSION` varchar(200) COLLATE utf8_bin NOT NULL COMMENT 'cron表达式',
  `TIME_ZONE_ID` varchar(80) COLLATE utf8_bin DEFAULT NULL COMMENT '时区ID',
  PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`),
  CONSTRAINT `qrtz_cron_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `qrtz_triggers` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;


CREATE TABLE `qrtz_fired_triggers` (
  `SCHED_NAME` varchar(120) COLLATE utf8_bin NOT NULL,
  `ENTRY_ID` varchar(95) COLLATE utf8_bin NOT NULL,
  `TRIGGER_NAME` varchar(200) COLLATE utf8_bin NOT NULL,
  `TRIGGER_GROUP` varchar(200) COLLATE utf8_bin NOT NULL,
  `INSTANCE_NAME` varchar(200) COLLATE utf8_bin NOT NULL,
  `FIRED_TIME` bigint(13) NOT NULL,
  `SCHED_TIME` bigint(13) NOT NULL,
  `PRIORITY` int(11) NOT NULL,
  `STATE` varchar(16) COLLATE utf8_bin NOT NULL,
  `JOB_NAME` varchar(200) COLLATE utf8_bin DEFAULT NULL,
  `JOB_GROUP` varchar(200) COLLATE utf8_bin DEFAULT NULL,
  `IS_NONCONCURRENT` varchar(1) COLLATE utf8_bin DEFAULT NULL,
  `REQUESTS_RECOVERY` varchar(1) COLLATE utf8_bin DEFAULT NULL,
  PRIMARY KEY (`SCHED_NAME`,`ENTRY_ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;


CREATE TABLE `qrtz_locks` (
  `SCHED_NAME` varchar(120) COLLATE utf8_bin NOT NULL,
  `LOCK_NAME` varchar(40) COLLATE utf8_bin NOT NULL,
  PRIMARY KEY (`SCHED_NAME`,`LOCK_NAME`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;


CREATE TABLE `qrtz_paused_trigger_grps` (
  `SCHED_NAME` varchar(120) COLLATE utf8_bin NOT NULL,
  `TRIGGER_GROUP` varchar(200) COLLATE utf8_bin NOT NULL,
  PRIMARY KEY (`SCHED_NAME`,`TRIGGER_GROUP`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

CREATE TABLE `qrtz_scheduler_state` (
  `SCHED_NAME` varchar(120) COLLATE utf8_bin NOT NULL COMMENT '调度器名称,集群名',
  `INSTANCE_NAME` varchar(200) COLLATE utf8_bin NOT NULL COMMENT '集群中实例ID',
  `LAST_CHECKIN_TIME` bigint(13) NOT NULL COMMENT '上次检查的时间',
  `CHECKIN_INTERVAL` bigint(13) NOT NULL COMMENT '检查时间间隔',
  PRIMARY KEY (`SCHED_NAME`,`INSTANCE_NAME`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

CREATE TABLE `qrtz_simple_triggers` (
  `SCHED_NAME` varchar(120) COLLATE utf8_bin NOT NULL,
  `TRIGGER_NAME` varchar(200) COLLATE utf8_bin NOT NULL,
  `TRIGGER_GROUP` varchar(200) COLLATE utf8_bin NOT NULL,
  `REPEAT_COUNT` bigint(7) NOT NULL,
  `REPEAT_INTERVAL` bigint(12) NOT NULL,
  `TIMES_TRIGGERED` bigint(10) NOT NULL,
  PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`),
  CONSTRAINT `qrtz_simple_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `qrtz_triggers` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

CREATE TABLE `qrtz_simprop_triggers` (
  `SCHED_NAME` varchar(120) COLLATE utf8_bin NOT NULL,
  `TRIGGER_NAME` varchar(200) COLLATE utf8_bin NOT NULL,
  `TRIGGER_GROUP` varchar(200) COLLATE utf8_bin NOT NULL,
  `STR_PROP_1` varchar(512) COLLATE utf8_bin DEFAULT NULL,
  `STR_PROP_2` varchar(512) COLLATE utf8_bin DEFAULT NULL,
  `STR_PROP_3` varchar(512) COLLATE utf8_bin DEFAULT NULL,
  `INT_PROP_1` int(11) DEFAULT NULL,
  `INT_PROP_2` int(11) DEFAULT NULL,
  `LONG_PROP_1` bigint(20) DEFAULT NULL,
  `LONG_PROP_2` bigint(20) DEFAULT NULL,
  `DEC_PROP_1` decimal(13,4) DEFAULT NULL,
  `DEC_PROP_2` decimal(13,4) DEFAULT NULL,
  `BOOL_PROP_1` varchar(1) COLLATE utf8_bin DEFAULT NULL,
  `BOOL_PROP_2` varchar(1) COLLATE utf8_bin DEFAULT NULL,
  PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`),
  CONSTRAINT `qrtz_simprop_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `qrtz_triggers` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

create index idx_qrtz_j_req_recovery on qrtz_job_details(SCHED_NAME,REQUESTS_RECOVERY);
create index idx_qrtz_j_grp on qrtz_job_details(SCHED_NAME,JOB_GROUP);

create index idx_qrtz_t_j on qrtz_triggers(SCHED_NAME,JOB_NAME,JOB_GROUP);
create index idx_qrtz_t_jg on qrtz_triggers(SCHED_NAME,JOB_GROUP);
create index idx_qrtz_t_c on qrtz_triggers(SCHED_NAME,CALENDAR_NAME);
create index idx_qrtz_t_g on qrtz_triggers(SCHED_NAME,TRIGGER_GROUP);
create index idx_qrtz_t_state on qrtz_triggers(SCHED_NAME,TRIGGER_STATE);
create index idx_qrtz_t_n_state on qrtz_triggers(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP,TRIGGER_STATE);
create index idx_qrtz_t_n_g_state on qrtz_triggers(SCHED_NAME,TRIGGER_GROUP,TRIGGER_STATE);
create index idx_qrtz_t_next_fire_time on qrtz_triggers(SCHED_NAME,NEXT_FIRE_TIME);
create index idx_qrtz_t_nft_st on qrtz_triggers(SCHED_NAME,TRIGGER_STATE,NEXT_FIRE_TIME);
create index idx_qrtz_t_nft_misfire on qrtz_triggers(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME);
create index idx_qrtz_t_nft_st_misfire on qrtz_triggers(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_STATE);
create index idx_qrtz_t_nft_st_misfire_grp on qrtz_triggers(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_GROUP,TRIGGER_STATE);

create index idx_qrtz_ft_trig_inst_name on qrtz_fired_triggers(SCHED_NAME,INSTANCE_NAME);
create index idx_qrtz_ft_inst_job_req_rcvry on qrtz_fired_triggers(SCHED_NAME,INSTANCE_NAME,REQUESTS_RECOVERY);
create index idx_qrtz_ft_j_g on qrtz_fired_triggers(SCHED_NAME,JOB_NAME,JOB_GROUP);
create index idx_qrtz_ft_jg on qrtz_fired_triggers(SCHED_NAME,JOB_GROUP);
create index idx_qrtz_ft_t_g on qrtz_fired_triggers(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP);

create index idx_qrtz_ft_tg on qrtz_fired_triggers(SCHED_NAME,TRIGGER_GROUP);

各个表的详细作用如下:

  • qrtz_job_details:记录每个任务的详细信息;
  • qrtz_triggers:记录每个触发器的详细信息;
  • qrtz_corn_triggers:记录 cronTrigger 的详细信息;
  • qrtz_scheduler_state:记录调度器(每个机器节点)的生命状态;
  • qrtz_fired_triggers:记录每个正在执行的触发器;
  • qrtz_locks:记录程序的悲观锁(防止多个机器节点同时执行同一个定时任务);

同时也可以在引入的 Quartz 包下的 quartz\2.3.2\quartz-2.3.2.jar!\org\quartz\impl\jdbcjobstore\tables_mysql_innodb.sql 找到对应的 SQL脚本执行。


Quartz 源码分析

以下主要在学习 Quartz 过程中对核心组件的源码的分析,较简陋:

Job/JobDetail/JobDataMap————任务实例/任务定义/任务数据

// 任务执行过程
public static void test2() {
    try {
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
        scheduler.start();
        // 通过 usingJobData 设置该任务需要的数据
        JobDetail detail = JobBuilder.newJob(PlayGameJob.class)
                .withIdentity("myJob", "group1")
                .usingJobData("gameName", "GTA5")
                .usingJobData("gamePrice", 5.5f)
                .build();

        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("myJob", "group1")
                .build();

        scheduler.scheduleJob(detail, trigger);
        Thread.sleep(10000);
        scheduler.shutdown();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

// 具体的任务实例
public class PlayGameJob implements Job {
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        JobKey key = context.getJobDetail().getKey();

        // 获取该任务的数据
        JobDataMap dataMap = context.getJobDetail().getJobDataMap();
        String gameName = dataMap.getString("gameName");
        Float gamePrice = dataMap.getFloat("gamePrice");
        System.out.println("GameName: "+gameName + " ,GamePrice: "+gamePrice);
    }
}
  1. 每次向调度器提交一个任务后,调度器就知道需要执行什么任务,只需要在构建 JobDetail 时提供任务的类型即可(HelloJob.Class)。
  2. 在触发器被触发,调用 execute 方法前会创建该任务类的一个新实例,在执行完任务后对该实例的引用就会被丢弃,然后该实例将被垃圾回收掉。
  3. JobDataMap 是用来保存任务执行时的数据及状态,JobDataMap 是 Java 中 Map 接口的一个实现,具有一些用于存储和检索原始类型数据的方法。如果任务在执行时需要一些数据,则可以在任务实例说明 JobDetail 中添加相关参数,在任务调用 execute() 执行时再从 JobDetail 中获取到 JobDataMap 来获取任务相关参数;
  4. 不仅可以在 JobDetail 中添加相关参数,在 Trigger 中也可以设计相应的参数。有这样一个场景,在调度器中已经有一个 Job 了,但是想让不同的 Trigger 去触发该 Job,在每个 Trigger 触发时你想要不同的数据传入这个 Job,那么就可以用到 Trigger 携带的 JobDataMap 了;

如何理解 JobDetail 和 Job:

JobDetail、Job 均为接口,Job 是任务实例,JobDetail是任务定义,当 Trigger 触发时,Scheduler 将加载与其关联的 JobDetail,并通过 Scheduler 上配置的 JobFactory 实例化它所引用的任务类。默认的 JobFactory 只是在任务类上调用newInstance() ,然后尝试在与 JobDataMap 中键的名称匹配的类中的属性名,进而调用 setter 方法将 JobDataMap 中的值赋值给对应的属性。

/**
 * 绑定任务实例和触发器操作
 */
    public Date scheduleJob(JobDetail jobDetail,
            Trigger trigger) throws SchedulerException {
        validateState();

        if (jobDetail == null) {
            throw new SchedulerException("JobDetail cannot be null");
        }
        
        if (trigger == null) {
            throw new SchedulerException("Trigger cannot be null");
        }
        
        if (jobDetail.getKey() == null) {
            throw new SchedulerException("Job's key cannot be null");
        }

        if (jobDetail.getJobClass() == null) {
            throw new SchedulerException("Job's class cannot be null");
        }
        
        OperableTrigger trig = (OperableTrigger)trigger;

        if (trigger.getJobKey() == null) {
            trig.setJobKey(jobDetail.getKey());
        } else if (!trigger.getJobKey().equals(jobDetail.getKey())) {
            throw new SchedulerException(
                "Trigger does not reference given job!");
        }

        trig.validate();

        Calendar cal = null;
        if (trigger.getCalendarName() != null) {
            cal = resources.getJobStore().retrieveCalendar(trigger.getCalendarName());
        }
        // 获取任务提交时间
        Date ft = trig.computeFirstFireTime(cal);

        if (ft == null) {
            throw new SchedulerException(
                    "Based on configured schedule, the given trigger '" + trigger.getKey() + "' will never fire.");
        }
        // 存储 JobDetail、Trigger对象到 JobStore 对象中
        resources.getJobStore().storeJobAndTrigger(jobDetail, trig);
        // 添加任务监听 SchedulerListener
        notifySchedulerListenersJobAdded(jobDetail);
        // 通知调度线程当前任务的下一次执行时间
        notifySchedulerThread(trigger.getNextFireTime().getTime());
        // 通知所有调度监听者
        notifySchedulerListenersSchduled(trigger);

        return ft;
    }

Scheduler 调度器

SchedulerRepository:

SchedulerSignaler:


JobDataMap 任务执行状态


Trigger 触发器

Trigger 公共属性:

  • jobKey:作为 Trigger 触发时应执行的任务标识;
  • startTime:记录首次触发的时间,对于某些触发器,它指定触发器应该在何时触发;
  • endTime:触发器不再生效的时间;
  • priority:任务优先级,值越大则表明优先级越高;
  • misfireInstruction:错失触发指令,某些特殊情况下导致触发器没有触发,就会执行该指令;当调度器启动时就会先检查有没有错过触发的触发器,有的话就优先基于触发器配置错失触发指令来更新触发器的信息。

SimpleTrigger 触发器

SimpleTrigger 触发器适用于在特定的时间执行一次任务,或者在特定时间执行一次接着定时执行。
包含如下属性:

  • startTime:开始时间;
  • endTime:结束时间;
  • repeatCount:重复次数;
  • repeatInterval:重复的时间间隔;

SimpleTrigger 的创建方式如下:

// 通过 TriggerBuilder、SimpleScheduleBuilder 来构建 SimpleTrigger
SimpleTrigger trigger5 = TriggerBuilder.newTrigger()
        .withIdentity("trigger6", "group1")
        .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInMinutes(5).repeatForever()
                .withMisfireHandlingInstructionNextWithExistingCount())
                .build();

SimpleTrigger 构建时使用到了生成器模式,它将 Trigger 的构建算法抽离出来,交给TriggerBuilderbuild() 方法去实现;然后具体的表现可以在生成时动态切换。

public T build() {
    // 填充 scheduleBuilder 对象
    if(scheduleBuilder == null)
        scheduleBuilder = SimpleScheduleBuilder.simpleSchedule();

    // 填充 MutableTrigger 对象,包括起止时间、描述信息等等
    MutableTrigger trig = scheduleBuilder.build();
    trig.setCalendarName(calendarName);
    trig.setDescription(description);
    trig.setStartTime(startTime);
    trig.setEndTime(endTime);

    // 设置Trigger优先级、TriggerKey
    if(key == null)
        key = new TriggerKey(Key.createUniqueName(null), null);
    trig.setKey(key); 
    if(jobKey != null)
        trig.setJobKey(jobKey);
    trig.setPriority(priority);
    
    // 设置jobDataMap
    if(!jobDataMap.isEmpty())
        trig.setJobDataMap(jobDataMap);
    
    return (T) trig;
}

SimpleTrigger 的使用示例:

    public static void test3() {
        try {
            Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
            scheduler.start();

            // 绑定任务
            JobDetail detail = JobBuilder.newJob(PlayGameJob.class)
                    .withIdentity("job1", "group1")
                    .usingJobData("gameName", "GTA5")
                    .usingJobData("gamePrice", 5.5f)
                    .build();

            // 1.给定时间触发,不重复
            Date startAt = DateBuilder.dateOf(22, 30, 0);
            SimpleTrigger trigger = (SimpleTrigger) TriggerBuilder.newTrigger()
                    .withIdentity("trigger1", "group1")
                    .startAt(startAt)
                    .forJob("job1", "group1").build();

            // 2.给定时间触发,每10秒重复触发10次
            SimpleTrigger trigger1 = TriggerBuilder.newTrigger()
                    .withIdentity("trigger2", "group1")
                    .startAt(startAt)
                    .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(10).withRepeatCount(10))
                    .forJob(detail).build();

            // 3.构建一个给定时刻触发任务,在未来五分钟内触发一次
            Date futureDate = DateBuilder.futureDate(5, DateBuilder.IntervalUnit.SECOND);
            JobKey jobKey = detail.getKey();
            SimpleTrigger trigger2 = (SimpleTrigger) TriggerBuilder.newTrigger()
                    .withIdentity("trigger3", "group1")
                    .startAt(futureDate)
                    .forJob(jobKey).build();

            // 4.构建一个给定时刻触发任务,每5分钟触发一次,直到晚上12点
            SimpleTrigger trigger3 = TriggerBuilder.newTrigger()
                    .withIdentity("trigger4", "group1")
                    .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                            .withIntervalInMinutes(5)
                            .repeatForever())
                    .endAt(DateBuilder.dateOf(23, 0, 0)).build();

            // 5.构建一个任意时刻触发的任务,然后每下一个小时整点触发,重复每2小时一次
            SimpleTrigger trigger4 = TriggerBuilder.newTrigger()
                    .withIdentity("trigger5", "group1")
                    .startAt(DateBuilder.evenHourDate(null))
                    .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInHours(2).repeatForever())
                    .forJob("job1", "group1")
                    .build();

            // 6.构建一个带有Misfire指令的触发器
            SimpleTrigger trigger5 = TriggerBuilder.newTrigger()
                    .withIdentity("trigger6", "group1")
                    .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInMinutes(5).repeatForever()
                            .withMisfireHandlingInstructionNextWithExistingCount())
                            .build();
            }
    }

错失触发指令

当 Trigger 由于调度器异常关闭、线程池无可用线程或者其它原因导致错失触发任务时,如果错失触发的时间超过了在 quartz.properties 文件中的 org.quartz.jobStore.misfireThreshold = 12000 配置的最大限度的任务超时触发时间, 那么 Quartz 就需要执行错失触发指令。

在代码中存在如下的 Misfire 指令,其中 MISFIRE_INSTRUCTION_FIRE_ONCE_NOWCronTrigger 的默认触发策略,MISFIRE_INSTRUCTION_SMART_POLICY SimpleTrigger 使用的策略,具体细节如下:

  1. MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY:所有 misfire 任务立刻执行(比如说如果是 5:00 的misfire,6:15线程恢复后,5:00和6:00的 misfire 会立刻执行);
  2. MISFIRE_INSTRUCTION_FIRE_ONCE_NOWCornTrigger的默认策略,合并部分misfire,正常执行下一个周期的任务(比如说如果是 5:00 的misfire,6:15线程恢复后只会立刻执行一次misfire);
  3. MISFIRE_INSTRUCTION_DO_NOTHING:所有 misfire 都不管,执行下一个周期的任务(比如说 5:00 的misfire,6:15线程恢复后,只会执行7:00 的misfire);
  4. MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT:立即执行,并执行指定次数;
  5. MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT:立即执行,并且错过的机会作废;
  6. MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT:在下一个激活点执行,并且错过的执行机会作废;
  7. MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT:在下一个激活点执行,并且执行指定次数。

Trigger 在构建时可以通过 withMisfireHanlingInstructionxxx 来指定使用哪个错误触发指令启动:


SpringBoot 整合 Quartz 配置任务持久化

1. 依赖引入

主要用到了 mysql、druid连接池、jdbc、quartz 相关配置:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 实现对 Quartz 的自动化配置 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</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>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.6</version>
</dependency>

2. 主配置文件 application.yaml

spring:
  datasource:
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/tb_test?serverTimezone=GMT%2B8
      username: root
      password: xjx123456
  quartz:
    # use mysql save data
    job-store-type: jdbc
    # scheduler node name
#    scheduler-name: xjxScheduler
    # whether to wait for job compelete before container close
    wait-for-jobs-to-complete-on-shutdown: true
    jdbc:
      # whether to use automitic_sql_initialize, if false initialize
      initialize-schema: never
    properties:
      org:
        quatrz:
          # configurations about jobStore
          jobStore:
            dataSource: druidDataSource
            class: org.quartz.impl.jdbcjobstore.JobStoreTX
            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
            tablePrefix: QRTZ_
            isClustered: true
            clusterCheckinInterval: 1000
            useProperties: false
          threadPool:
            threadCount: 25
            threadPriority: 5
            class: org.quartz.simpl.SimpleThreadPool

3. 创建任务及调度器绑定

实现 Job 接口分别创建了两个任务 FirstJobSecondJob

@Slf4j
public class FirstJob implements Job {
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        String now = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now());
        log.info("当前时间:"+now);
    }
}

@Slf4j
public class SecondJob implements Job {
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        String now = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now());
        log.info("SecondJob执行,当前时间:"+now);
    }
}

SpringBoot 在整合时,调度器的绑定有两种方法,分别是自动配置和手动配置:

自动配置

/**
 * 自动配置 Scheduler 绑定,分别实现 JobDetail、Trigger两个Bean,Spring会自动绑定这两个Bean对象
 */
@Configuration
public class QuartzConfig {
    private static final String ID = "SUMMERDAY";

    @Bean
    public JobDetail jobDetail() {
        return JobBuilder.newJob(FirstJob.class)
                .withIdentity(ID + "01")
                .storeDurably()
                .build();
    }

    @Bean
    public Trigger trigger1() {
        SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
                .withIntervalInSeconds(5)
                .repeatForever();

        return TriggerBuilder.newTrigger()
                .forJob(jobDetail())
                .withIdentity(ID + "01Trigger")
                .withSchedule(scheduleBuilder)
                .build();
    }
}

手动绑定

/**
 * 手动配置 Scheduler 绑定,手动绑定需要手动初始化 JobDetail、Trigger对象,然后结合注入的 Scheduler对象绑定返回
 */
@Component
public class JobInit implements ApplicationRunner {
    private static final String ID = "SUMMERDAY";

    @Autowired
    private Scheduler scheduler;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        //创建任务信息
        JobDetail jobDetail = JobBuilder.newJob(SecondJob.class)
                .withIdentity(ID + " 02")
                .storeDurably()
                .build();
        CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/10 * * * * ? *");

        //创建任务触发器
        Trigger trigger = TriggerBuilder.newTrigger()
                .forJob(jobDetail)
                .withIdentity(ID + "02Trigger")
                .withSchedule(scheduleBuilder)
                .startNow().build();

        Set<Trigger> set = new HashSet<>();
        set.add(trigger);
        // boolean replace表示启动时对数据库中的 quartz 任务进行覆盖
        scheduler.scheduleJob(jobDetail, set, true);
    }
}

4. 创建数据库表文件

Quartz 的 org\quartz-scheduler\quartz\2.3.2\quartz-2.3.2.jar!\org\quartz\impl\jdbcjobstore\tables_mysql_innodb.sql 下有对应的SQL脚本,执行即可,执行后创建了上面讲述到的各个表。


任务并/串行执行

存在这样一种情况,如果是多线程我们能够控制同一时刻相同的任务只能有一个在执行。如果执行频率定为 30s,30s 到时后可能任务还未结束,此时又启动一个任务,而我们希望任务是串行执行的,也就是前一个任务执行结束后才执行下一个任务。

@Component
public class ThirdJob implements Job {
    private Logger logger = LoggerFactory.getLogger(ThirdJob.class);

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        logger.info("[数据库配置定时]-[开始]");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        logger.info("[数据库配置定时]-[结束]");
    }
}

查看数据库 qrtz_job_details 表内容,它展示了当前正在执行任务的情况,若表中的 IS_NONCONCURRENT 字段值为0表示任务可以并发执行,为1表示不能并发执行。

在具体的任务实例类上添加 @DisallowConcurrentExecution 注解即可使任务串行执行:

/**
 * 定义具体的任务类
 */
@Component
@DisallowConcurrentExecution
public class ThirdJob implements Job {
    private Logger logger = LoggerFactory.getLogger(ThirdJob.class);

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        logger.info("[数据库配置定时]-[开始]");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        logger.info("[数据库配置定时]-[结束]");
    }
}

/**
 * 自动配置 Scheduler 绑定
 */
@Configuration
public class QuartzConfig {
    private static final String ID = "SUMMERDAY";

    @Bean
    public JobDetail jobDetail() {
        return JobBuilder.newJob(ThirdJob.class)
                .withIdentity(ID + "03")
                .storeDurably()
                .build();
    }

    @Bean
    public Trigger trigger1() {
        SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
                .withIntervalInSeconds(5)
                .repeatForever();

        return TriggerBuilder.newTrigger()
                .forJob(jobDetail())
                .withIdentity(ID + "03Trigger")
                .withSchedule(scheduleBuilder)
                .build();
    }
}

再启动 SpringBoot 项目,查看数据库发现该字段变为1,控制台打印的任务执行情况也是串行执行: