Quartz

一、Quartz的基本配置

1. Quartz的核心元素

Quartz调度依靠的三大核心元素就是:Scheduler、Trigger、Job。

1. Job(任务)

作用:具体要执行的业务逻辑,比如:发送短信、发送邮件、访问数据库、同步数据等。

2. Trigger(触发器)

作用:用来定义Job(任务)触发条件、触发时间,触发间隔,终止时间等。
四大类型:SimpleTrigger、CornTrigger、DateIntervalTrigger、NthIncludedDayTrigger。

3. scheduler(调度器)

作用:Scheduler启动Trigger去执行Job。
类型:Scheduler由scheduler工厂创建:DirectSchedulerFactory 或者 StdSchedulerFactory。
第二种工厂StdSchedulerFactory使用较多,因为 DirectSchedulerFactory 使用起来不够方便,需要作许多详细的手工编码设置。
Scheduler 主要有三种:RemoteMBeanScheduler, RemoteScheduler 和 StdScheduler。

2. Quartz的准备工作

2.1. maven依赖

<!--quartz相关依赖-->
<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
    <version>2.2.1</version>
</dependency>

<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz-jobs</artifactId>
    <version>2.2.1</version>
</dependency>
>>>>SpringBoot替换为:>>>>
<!--quartz依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

2.2. Quartz的配置文件

在Quartz JAR文件的org.quartz包下就包含一个quartz.properties属性配置文件并提供了默认属性。如果需要调整默认配置,可以在类路径下建立一个新的quartz.properties,它将自动被Quartz加载并覆盖默认的设置。

2.2.1 文件的加载位置

需要注意的是:配置文件一般为quartz.properties文件,但是如果使用yml文件格式的配置,则quartz.properties里面的配置会失效;

【配置文件】默认:优先顺序Classpath:quartz.properties-->org/quartz/quartz.properties

【程序内指定】在StdSchedulerFactory.getScheduler()之前使用StdSchedulerFactory.initialize(xx)

2.2.2Quartz的配置信息

注:QuartzProperties类读取的就是我们yml文件中的配置。

1. 基础配置

scheduler的配置信息

#可以为任意字符串,对于scheduler来说此值没有意义,但是可以区分同一系统中多个不同的实例,
#如果使用了集群的功能,就必须对每一个实例使用相同的名称,这样使这些实例“逻辑上”是同一个scheduler。
org.quartz.scheduler.instanceName = JobScheduler
#可以是任意字符串,但如果是集群,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

2. 调度器线程池配置

#线程池的实例类,(一般使用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.scheduler.makeSchedulerThreadDaemon: true

3. Configure JobStore 作业存储配置

3.1. RAMJobStore

将schedule相关信息保存在RAM中,轻量级,速度快,遗憾的是应用重启时相关信息都将丢失。

org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
#最大能忍受的触发超时时间,如果超时则认为“失误”
org.quartz.jobStore.misfireThreshold = 60000

3.2. JDBC-JobStore

将schedule相关信息保存在RDB中,有两种实现:JobStoreTX和JobStoreCMT

前者为application自己管理事务;
后者为application server管理事务,即全局JTA;

JDBCJobStore终于把数据持久化起来了,这个也是使用最广泛的数据存储方式。在于Spring集成后都不需要自己配置一遍数据库连接了。

配置方式以及默认值:

#选择JDBC的存储方式
org.quartz.jobStore.class= org.quartz.impl.jdbcjobstore.JobStoreTX
#类似于Hibernate的dialect,用于处理DB之间的差异,StdJDBCDelegate能满足大部分的DB(授权)
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
#存储相关信息表的前缀
org.quartz.jobStore.tablePrefix = QRTZ_
#这个值必须datasource的配置信息
org.quartz.jobStore.dataSource = myDS
#JobDataMaps是否都为String类型
#(若是true的话,便可不用让更复杂的对象以序列化的形式保存到BLOB列中。以防序列化可能导致的版本号问题)
org.quartz.jobStore.useProperties = false
#最大能忍受的触发超时时间,如果超时则认为“失误”
org.quartz.jobStore.misfireThreshold = 60000
#是否是应用在集群中,当应用在集群中时必须设置为TRUE,否则会出错。
#如果有多个Quartz实例在用同一套数据库时,必须设置为true。
org.quartz.jobStore.isClustered=False
#只用于设置了isClustered为true的时候,设置一个频度(毫秒),用于实例报告给集群中的其他实例。
#这会影响到侦测失败实例的敏捷度。
org.quartz.jobStore.clusterCheckinInterval =15000
#这是JobStore能处理的错过触发的Trigger的最大数量。处理太多(2打)很快就会导致数据库表被锁定够长的时间,
#这样会妨碍别的(还未错过触发)trigger执行的性能。
org.quartz.jobStore.maxMisfiresToHandleAtATime=20
#设置这个参数为true会告诉Quartz从数据源获取连接后不要调用它的setAutoCommit(false)方法。
#在少数情况下是有用的,比如有一个驱动本来是关闭的,但是又调用这个关闭的方法。但是大部分情况下驱动都要求调用setAutoCommit(false)
org.quartz.jobStore.dontSetAutoCommitFalse=false
#这必须是一个从LOCKS表查询一行并对这行记录加锁的SQL。假设没有设置,默认值如下。
#{0}会在运行期间被前面配置的TABLE_PREFIX所代替
org.quartz.jobStore.selectWithLockSQL=SELECT * FROM {0}LOCKS WHERE LOCK_NAME = ? FOR UPDATE
#值为true时告知Quartz(当使用JobStoreTX或CMT)调用JDBC连接的setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE) 方法。这有助于某些数据库在高负载和长时间事务时锁的超时。
org.quartz.jobStore.txIsolationLevelSerializable=false

4. 数据库配置

datasource的相关信息全部定义与quartz.properties中,quartz自己创建datasource。

org.quartz.dataSource.NAME.driver
org.quartz.dataSource.NAME.URL
org.quartz.dataSource.NAME.user
org.quartz.dataSource.NAME.password
org.quartz.dataSource.NAME.maxConnections

需要注意的是,#NAME字段必须与$@org.quartz.jobStore.dataSource保持一致

但是一般我们的数据库连接池一般使用第三方连接池,那么就会导致org.quartz.jobStore.dataSource=#NAME无法配置!!可以使用quartz.job-store-type=jdbc替代。

2.2.3 SpringBoot2.x整合Quartz的配置文件

自动加载的源码:org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration

spring:
  datasource:
    name: mysql_test
    type: com.alibaba.druid.pool.DruidDataSource
    #druid相关配置
    druid:
      #监控统计拦截的filters
      filter: stat,config
#      driver-class-name: com.mysql.cj.jdbc.Driver
      #基本属性
      url: jdbc:mysql://localhost:3306/mydb
      username: root
      password: 123456
      #初始化连接数
      initial-size: 10
      #最小活跃连接数
      min-size: 5
      #最大活跃连接数
      max-active: 20
      #获取连接的等待时间
      max-wait: 60000
      #间隔多久进行一次检查,检查需要关闭的空闲连接
      time-between-eviction-runs-millis: 60000
      #一个连接在池中最小的生存时间(5分钟)
      min-evictable-idle-time-millis: 300000
      validation-query: SELECT 'X'
      # 验证空闲的连接,若无法验证,则删除连接
      test-while-idle: true
      # 不检测池中连接的可用性(默认false)
      # 导致的问题是,若项目作为服务端,数据库连接被关闭时,客户端调用就会出现大量的timeout
      test-on-borrow: false
      #在返回连接池之前是否验证对象
      test-on-return: false
      #打开PSCache,并指定每个连接上PSCache的大小。oracle设为true,mysql设为false。分库分表较多推荐设置为false
      #第三发连接池在使用的时候,获取到Connection后,使用完毕,调用关闭方法,并没有将Connection关闭,只是放回到连接池中
      #如果调用这个方法,而没有手动关闭PreparedStatement,就可能造成内存溢出,但是JDK1.7实现了AutoCloseable接口,就不需要关闭了
      pool-prepared-statements: false
      max-pool-prepared-statement-per-connection-size: 20
      # connection-properties:
      use-unfair-lock: true
  quartz:
    #相关属性配置
    properties:
      org:
        quartz:
          scheduler:
            # 集群名,区分同一系统的不同实例,若使用集群功能,则每一个实例都要使用相同的名字
            instanceName: clusteredScheduler
            # 若是集群下,每个instanceId必须唯一
            instanceId: AUTO
          threadPool:
            #一般使用这个便可
            class: org.quartz.simpl.SimpleThreadPool
            #线程数量,不会动态增加
            threadCount: 10
            threadPriority: 5
            threadsInheritContextClassLoaderOfInitializingThread: true
          jobStore:
            #选择JDBC的存储方式
            class: org.quartz.impl.jdbcjobstore.JobStoreTX
            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
            tablePrefix: QRTZ_
            useProperties: false
            isClustered: true
            clusterCheckinInterval: 15000
    job-store-type: jdbc
    #是否等待任务执行完毕后,容器才会关闭
    wait-for-jobs-to-complete-on-shutdown=false
    #配置的job是否覆盖已经存在的JOB信息
    overwrite-existing-jobs: false

1. 讲一下最后两个配置

  1. wait-for-jobs-to-complete-on-shutdown:Quartz默认false,是否等待任务运行完毕后关闭Spring容器,若是为false的情况下,可能出现java.lang.IllegalStateException: JobStore is shutdown - aborting retry异常,推荐开启。
  2. overwrite-existing-jobs:这个是配置文件的job是否会覆盖数据库正在运行的job。quartz启动之后,会以数据库的为准,若该属性为false,则配置文件修改后不会起作用。

2. 线程池配置不生效问题及原因

配置线程池参数,无论怎样都不生效。经过debug得知。源码:org.quartz.impl.StdSchedulerFactory#instantiate(),由于线程池的参数有误,导致配置的是默认值。

image-20220827135044071

相关参考:

spring整合quartz并持久化

在spring quartz中,造成 misfired job 的原因有哪些

Quartz-job的quartz.properties配置文件说明

(精)Quartzs -- Quartz.properties 配置

(译)quartz_properties文件配置之主要配置

二、jobstore数据库表字段详解

从上节我们知道JobStore的存储方式有两种:

  • RAMJobStore:将scheduler存储在内存中,但是重启服务器信息会丢失。
  • JDBCJobStore:将scheduler存储在数据库中。

所有需要在数据库中创建数据库表,来完成scheduler数据的存储。

官网——quartz所需的sql地址

汇总:每张表的使命

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

1. qrtz_job_details

存储每一个已配置的Job的详细信息。

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的名字',
  `JOB_GROUP` varchar(200) COLLATE utf8_bin NOT NULL COMMENT '集群中job的所属组的名字',
  `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,quartz会把job持久化到数据库中',
  `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失败后,其他实例可以发现那些执行失败的Jobs,若是1,那么该Job会被其他实例重新执行,否则对应的Job只能释放等待下次触发',
  `JOB_DATA` blob COMMENT '一个blob字段,存放持久化job对象',
  PRIMARY KEY (`SCHED_NAME`,`JOB_NAME`,`JOB_GROUP`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='存储每一个已配置的 Job 的详细信息';

2. qrtz_triggers

存放配置的Trigger信息

注意:
1. 一个Job可以被多个Trigger绑定,但是一个Trigger只能绑定一个Job。

2. MISFIRE_INSTR字段表示misfire策略,cornTrigger和SimpleTrigger含有不同的含义,以cronTrigger为例,字段枚举如下:org.quartz.Trigger类下:

CREATE TABLE `qrtz_triggers` (
  `SCHED_NAME` varchar(120) COLLATE utf8_bin NOT NULL COMMENT '调度器名,和配置文件org.quartz.scheduler.instanceName保持一致',
  `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 'qrtz_job_details表job_name的外键',
  `JOB_GROUP` varchar(200) COLLATE utf8_bin NOT NULL COMMENT 'qrtz_job_details表job_group的外键',
  `DESCRIPTION` varchar(250) COLLATE utf8_bin DEFAULT NULL COMMENT '描述',
  `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 COMMENT '日历名称',
  `MISFIRE_INSTR` smallint(2) DEFAULT NULL COMMENT 'misfire处理规则,1代表【以当前时间为触发频率立刻触发一次,然后按照Cron频率依次执行】,
   2代表【不触发立即执行,等待下次Cron触发频率到达时刻开始按照Cron频率依次执行�】,
   -1代表【以错过的第一个频率时间立刻开始执行,重做错过的所有频率周期后,当下一次触发频率发生时间大于当前时间后,再按照正常的Cron频率依次执行】',
  `JOB_DATA` blob COMMENT 'JOB存储对象',
  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 COMMENT='存储已配置的 Trigger 的信息';

3. qrtz_corn_triggers

存放cron类型的触发器

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 '调度器名,qrtz_triggers表trigger_name的外键',
  `TRIGGER_GROUP` varchar(200) COLLATE utf8_bin NOT NULL COMMENT 'qrtz_triggers表trigger_group的外键',
  `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 COMMENT='存放cron类型的触发器';

4. qrtz_scheduler_state

存储所有节点的scheduler,会定期检查scheduler是否失效,启动多个scheduler,查询数据库:

image-20220827140348098

记录了最后最新的检查时间,在quartz.properties中设置了CHECKIN_INTERVAL为1000,也就是每秒检查一次;

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,配置文件中org.quartz.scheduler.instanceId的配置',
  `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 COMMENT='调度器状态';

5. qrtz_fired_triggers

存储已经触发的trigger相关信息,trigger随着时间的推移状态发生变化,直到最后trigger执行完成,从表中被删除;以SimpleTrigger为例重复3次执行为例。
相同的trigger和job,每触发一次都会创建一个实例;从刚被创建的ACQUIRED状态,到EXECUTING状态,最后执行完从数据库中删除;

image-20220827140644657

CREATE TABLE `qrtz_fired_triggers` (
  `SCHED_NAME` varchar(120) COLLATE utf8_bin NOT NULL COMMENT '调度器名称,集群名',
  `ENTRY_ID` varchar(95) COLLATE utf8_bin NOT NULL COMMENT '运行Id',
  `TRIGGER_NAME` varchar(200) COLLATE utf8_bin NOT NULL COMMENT '触发器名',
  `TRIGGER_GROUP` varchar(200) COLLATE utf8_bin NOT NULL COMMENT '触发器组',
  `INSTANCE_NAME` varchar(200) COLLATE utf8_bin NOT NULL COMMENT '集群中实例ID',
  `FIRED_TIME` bigint(13) NOT NULL COMMENT '触发时间',
  `SCHED_TIME` bigint(13) NOT NULL,
  `PRIORITY` int(11) NOT NULL COMMENT '线程优先级',
  `STATE` varchar(16) COLLATE utf8_bin NOT NULL COMMENT '状态',
  `JOB_NAME` varchar(200) COLLATE utf8_bin DEFAULT NULL COMMENT '任务名',
  `JOB_GROUP` varchar(200) COLLATE utf8_bin DEFAULT NULL COMMENT '任务组',
  `IS_NONCONCURRENT` varchar(1) COLLATE utf8_bin DEFAULT NULL COMMENT '是否并行',
  `REQUESTS_RECOVERY` varchar(1) COLLATE utf8_bin DEFAULT NULL COMMENT '是否恢复',
  PRIMARY KEY (`SCHED_NAME`,`ENTRY_ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='存储与已触发的 Trigger 相关的状态信息,以及相联 Job 的执行信息';

6. qrtz_simple_triggers

存储简单的trigger,包括重复次数,间隔,以及触发次数。
注意:TIMES_TRIGGERED用来记录执行了多少次了,此值被定义在SimpleTriggerImpl中,每次执行+1,这里定义的REPEAT_COUNT=5,实际情况会执行6次。因为第一次是在0开始。

CREATE TABLE `qrtz_simple_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 '触发器组',
  `REPEAT_COUNT` bigint(7) NOT NULL COMMENT '重复次数',
  `REPEAT_INTERVAL` bigint(12) NOT NULL COMMENT '重复间隔',
  `TIMES_TRIGGERED` bigint(10) NOT NULL COMMENT '已触发次数',
  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 COMMENT='存储简单的 Trigger,包括重复次数,间隔,以及已触的次数';

7. qrtz_simprop_triggers

存储CalendarIntervalTrigger和DailyTimeIntervalTrigger两种类型的触发器,使用CalendarIntervalTrigger做如下配置:

CalendarIntervalTrigger没有对应的FactoryBean,直接设置实现类CalendarIntervalTriggerImpl;指定的重复周期是1,默认单位是天,也就是每天执行一次。

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 COMMENT='存储CalendarIntervalTrigger和DailyTimeIntervalTrigger两种类型的触发器';

8. qrtz_blob_triggers

自定义的triggers使用blog类型进行存储,非自定义的triggers不会存放在此表中,Quartz提供的triggers包括:CronTrigger,CalendarIntervalTrigger,DailyTimeIntervalTrigger以及SimpleTrigger。

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;

9. qrtz_calendars

以 Blob 类型存储 Quartz 的 Calendar 信息

CREATE TABLE qrtz_calendars
(
  SCHED_NAME VARCHAR2(120) NOT NULL,
  CALENDAR_NAME  VARCHAR2(200) NOT NULL,
  CALENDAR BLOB NOT NULL,
  CONSTRAINT QRTZ_CALENDARS_PK PRIMARY KEY (SCHED_NAME,CALENDAR_NAME)
);

10. qrtz_paused_trigger_grps

存储已暂停的 Trigger 组的信息

CREATE TABLE qrtz_paused_trigger_grps
(
  SCHED_NAME VARCHAR2(120) NOT NULL,
  TRIGGER_GROUP  VARCHAR2(200) NOT NULL,
  CONSTRAINT QRTZ_PAUSED_TRIG_GRPS_PK PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP)
);

11. qrtz_locks

存储程序的悲观锁的信息(假如使用了悲观锁)

Quartz提供的锁表,为多个节点调度提供分布式锁,实现分布式调度,默认有2个锁:

  • STATE_ACCESS主要用在scheduler定期检查是否有效的时候,保证只有一个节点去处理已经失效的scheduler。
  • TRIGGER_ACCESS主要用在TRIGGER被调度的时候,保证只有一个节点去执行调度。
CREATE TABLE qrtz_locks
(
  SCHED_NAME VARCHAR2(120) NOT NULL,
  LOCK_NAME  VARCHAR2(40) NOT NULL,
  CONSTRAINT QRTZ_LOCKS_PK PRIMARY KEY (SCHED_NAME,LOCK_NAME)
);

Quartz的配置细节

附录:

  1. Quartz的2.*版本数据库建表语句
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,
  `JOB_NAME` varchar(200) COLLATE utf8_bin NOT NULL,
  `JOB_GROUP` varchar(200) COLLATE utf8_bin NOT NULL,
  `DESCRIPTION` varchar(250) COLLATE utf8_bin DEFAULT NULL,
  `JOB_CLASS_NAME` varchar(250) COLLATE utf8_bin NOT NULL,
  `IS_DURABLE` varchar(1) COLLATE utf8_bin NOT NULL,
  `IS_NONCONCURRENT` varchar(1) COLLATE utf8_bin NOT NULL,
  `IS_UPDATE_DATA` varchar(1) COLLATE utf8_bin NOT NULL,
  `REQUESTS_RECOVERY` varchar(1) COLLATE utf8_bin NOT NULL,
  `JOB_DATA` blob,
  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,
  `TRIGGER_NAME` varchar(200) COLLATE utf8_bin NOT NULL,
  `TRIGGER_GROUP` varchar(200) COLLATE utf8_bin NOT NULL,
  `JOB_NAME` varchar(200) COLLATE utf8_bin NOT NULL,
  `JOB_GROUP` varchar(200) COLLATE utf8_bin NOT NULL,
  `DESCRIPTION` varchar(250) COLLATE utf8_bin DEFAULT NULL,
  `NEXT_FIRE_TIME` bigint(13) DEFAULT NULL,
  `PREV_FIRE_TIME` bigint(13) DEFAULT NULL,
  `PRIORITY` int(11) DEFAULT NULL,
  `TRIGGER_STATE` varchar(16) COLLATE utf8_bin NOT NULL,
  `TRIGGER_TYPE` varchar(8) COLLATE utf8_bin NOT NULL,
  `START_TIME` bigint(13) NOT NULL,
  `END_TIME` bigint(13) DEFAULT NULL,
  `CALENDAR_NAME` varchar(200) COLLATE utf8_bin DEFAULT NULL,
  `MISFIRE_INSTR` smallint(2) DEFAULT NULL,
  `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,
  `TRIGGER_NAME` varchar(200) COLLATE utf8_bin NOT NULL,
  `TRIGGER_GROUP` varchar(200) COLLATE utf8_bin NOT NULL,
  `CRON_EXPRESSION` varchar(200) COLLATE utf8_bin NOT NULL,
  `TIME_ZONE_ID` varchar(80) COLLATE utf8_bin DEFAULT NULL,
  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,
  `INSTANCE_NAME` varchar(200) COLLATE utf8_bin NOT NULL,
  `LAST_CHECKIN_TIME` bigint(13) NOT NULL,
  `CHECKIN_INTERVAL` bigint(13) NOT NULL,
  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);

三、任务的并行/串行执行

若是多线程时,能够控制同一时刻相同的JOB只能有一个在执行。原因是由于定时执行频率是30s,然而可能30s后该job还没结束。结果另一个线程再次启用同一job下的方法。
我们希望前一个job结束后才能进行下一次的调用。那该如何设计呢?

1. 提出问题

任务类:

@Component
public class TestJob2 extends  CustomQuartzJobBean{
    private Logger logger = LoggerFactory.getLogger(TestJob2.class);

    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        logger.info("【数据库配置定时】-【开始】");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        logger.info("【数据库配置定时】-【结束】");
    }
}

但定时是[*/1 * * * * ?],即1s执行一次。

2019-07-04 14:44:21,010 INFO [51877] [clusteredScheduler_Worker-10] [] (TestJob2.java:24): 【数据库配置定时】-【开始】
2019-07-04 14:44:21,011 INFO [51878] [clusteredScheduler_Worker-8] [] (TestJob2.java:30): 【数据库配置定时】-【结束】
2019-07-04 14:44:22,009 INFO [52876] [clusteredScheduler_Worker-5] [] (TestJob2.java:30): 【数据库配置定时】-【结束】
2019-07-04 14:44:22,010 INFO [52877] [clusteredScheduler_Worker-1] [] (TestJob2.java:24): 【数据库配置定时】-【开始】
2019-07-04 14:44:23,011 INFO [53878] [clusteredScheduler_Worker-3] [] (TestJob2.java:24): 【数据库配置定时】-【开始】
2019-07-04 14:44:23,010 INFO [53877] [clusteredScheduler_Worker-10] [] (TestJob2.java:30): 【数据库配置定时】-【结束】

可以看到,此时的任务是并发执行。

image-20220827141623895

因为将scheduler存储在数据库中。由此我们可以看到该job【是否允许并发】字段为【no】。

2. 分析问题

Spring提供了两种配置JobDetail的方式,官方示例如下:

<!-- JobDetail配置 1 -->
<bean name="exampleJob" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
    <property name="jobClass" value="example.ExampleJob"/>
    <property name="jobDataAsMap">
        <map>
            <entry key="timeout" value="5"/>
        </map>
    </property>
</bean>
<!-- JobDetail配置 2 -->
<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
    <property name="targetObject" ref="exampleBusinessObject"/>
    <property name="targetMethod" value="doIt"/>
    <property name="concurrent" value="false"/>
</bean>
 JobDetailFactoryBean jobFactory = new JobDetailFactoryBean();
 jobFactory.setName(jobName);
 jobFactory.setBeanName(clazzName);  //包名+类名
 jobFactory.setJobClass((Class<? extends Job>) aClass);
 jobFactory.setGroup(jobGroup);
 jobFactory.setDurability(true);
 jobFactory.afterPropertiesSet();

 MethodInvokingJobDetailFactoryBean invokingJobDetailFactoryBean=new MethodInvokingJobDetailFactoryBean();
 invokingJobDetailFactoryBean.setConcurrent(false);
 invokingJobDetailFactoryBean.setName(jobName);
 invokingJobDetailFactoryBean.setGroup(jobGroup);
 invokingJobDetailFactoryBean.setTargetClass(aClass);
 invokingJobDetailFactoryBean.setTargetMethod("xxx");

首先说MethodInvokingJobDetailFactoryBean配置,按照官方的意思是:在简单情况下,如果定时任务只是需要调用某个Spring对象的某个方法,可以使用这个简单配置下,concurrent设置为false可以防止任务的并发执行。但是这种方式功能不强,作用不大。

2.1 JobDetailFactoryBean方式配置

首先,需要配置jobClass属性,jobClass需要传入一个job实现类。Spring推荐直接继承QuartzJobBean类,官方示例如下

package example;

public class ExampleJob extends QuartzJobBean {

    private int timeout;

    /**
     * Setter called after the ExampleJob is instantiated
     * with the value from the JobDetailFactoryBean (5)
     */
    public void setTimeout(int timeout) {
        this.timeout = timeout;
    }

    protected void executeInternal(JobExecutionContext ctx) throws JobExecutionException {
        // do the actual work
    }

}

executeInternal()中进行实际的逻辑

其次是配置jobDataAsMap属性,如上例中,jobDataAsMap配置了一个map

<entry key="timeout" value="5"/>

只要继承QuartzJobBean,就可以通过这种方式直接向Job属性中传递值。如上例中的:

private int timeout;

/**
     * Setter called after the ExampleJob is instantiated
     * with the value from the JobDetailFactoryBean (5)
     */
public void setTimeout(int timeout) {
    this.timeout = timeout;
}

然后在executeInternal()方法中就可以直接使用timeout的值了。

@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
    System.out.println(timeout);
}

2.2 配置Trigger和Scheduler

Quartz中常用的Trigger只有两种:

<bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean">
    <!-- see the example of method invoking job above -->
    <property name="jobDetail" ref="jobDetail"/>
    <!-- 10 seconds -->
    <property name="startDelay" value="10000"/>
    <!-- repeat every 50 seconds -->
    <property name="repeatInterval" value="50000"/>
</bean>

<!-- trigger 2 -->
<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
    <property name="jobDetail" ref="exampleJob"/>
    <!-- run every morning at 6 AM -->
    <property name="cronExpression" value="0 0 6 * * ?"/>
</bean>
 CronTriggerFactoryBean triggerFactoryBean = new 
 CronTriggerFactoryBean();
 triggerFactoryBean.setName("corn_" + clazzName);
 //jobDetails
 triggerFactoryBean.setJobDetail(jobFactory.getObject());
 triggerFactoryBean.setCronExpression(quartzCorn);
 triggerFactoryBean.setGroup(QUARTZ_TRIGGER_GROUP);
 triggerFactoryBean.afterPropertiesSet();
  • simpleTrigger每隔一段时间执行一次任务,jobDetail就是前面生成的jobDetail;startDelay表示系统启动起来后过多长时间开始执行任务,单位:毫秒;repeatInterval表示任务执行时间间隔,单位毫秒;
  • cornTrigger是corn表达式来执行的定时任务。

实际上,是登记qrtz_corn_triggers|qrtz_simple_triggersqrtz_triggers这张表

image-20220827142213303

3. 如何设置Quartz任务串行

MethodInvokingJobDetailFactoryBean中,含有一个concurrent属性,可以用来控制任务是否并行执行。但在JobDetailFactoryBean中并没有这个属性。并且由于MethodInvokingJobDetailFactoryBean功能差,在实际开发中使用的并不多。所以,如何防止JobDetailFactoryBean并行呢?

package example;

//只需要在Job类上加上这个注解即可
@DisallowConcurrentExecution
public class ExampleJob extends QuartzJobBean {

    private int timeout;

    /**
     * Setter called after the ExampleJob is instantiated
     * with the value from the JobDetailFactoryBean (5)
     */
    public void setTimeout(int timeout) {
        this.timeout = timeout;
    }

    protected void executeInternal(JobExecutionContext ctx) throws JobExecutionException {
        // do the actual work
    }

}

image-20220827142328590

在加上注解后,该表是否并行会在项目启动的时候,自动设置为1。不允许并行。

四、misfire处理机制

什么叫做misfire

在Quartz中,当一个持久化的触发器因为:
1. 调度器被关闭;
2. 线程池没有可用线程;
3. 项目重启;
4. 任务的串行执行;
而错过激活时间,就会发生激活失败(misfire)。

此时我们需要明确两个问题(1)如何判定激活失败;(2)如何处理激活失败;

1. 如何判定激活失败

Quartz框架(一)—Quartz的基本配置中,quartz.properties配置文件中含有一个属性是misfireThreshold(单位毫秒),用来指定调度引擎设置触发器超时的“临界值”。也就是说Quartz对于任务的超时是有容忍度的。只有超过这个容忍度才会判定位misfire。

quartz.properties的配置文件

#设置容忍度为12s
org.quartz.jobStore.misfireThreshold = 12000

Corn=[*/2 * * * * ?] 即每两秒循环一次

jobDetail每次执行需要7s

@Component
@DisallowConcurrentExecution
public class TestJob2 extends  CustomQuartzJobBean{
    private Logger logger = LoggerFactory.getLogger(TestJob2.class);

    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        logger.info("【数据库配置定时】-【开始】");
        try {
            Thread.sleep(7000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        logger.info("【数据库配置定时】-【结束】");
    }
}
任务编号 预定运行时刻 实际运行时刻 延迟量(秒)
1 17:54:00 17:54:00 0
2 17:54:02 17:54:07 5
3 17:54:04 17:54:14 10
4 17:54:06 17:54:21 misfire

从表中可以看到,每一次任务的延迟都是5s作用,该延迟量不断累积,并且与misfireThreshold比较,直到在17:54:21时发生了misfire,那么17:54:21第4次任务会不会执行呢,答案是不一定的,取决于配置。

2. 激活失败的处理

激活失败指令(Misfire Instruction[因死抓可神 指令])是触发器的一个重要配置。所有类型的触发器都有一个默认的指令:

Trigger.MISFIRE_INSTRUCTION_SMART_POLICY

但是这个“机智策略(policy [跑了谁] )”对于不同类型的触发器其具体行为是不同的。

misfire Instruction在qutz_triggers表中的字段。

image-20220827142756419

代码中设置misfire策略的方式。

2.1 quartz中CornTrigger使用的策略

//所有的misfile任务马上执行
public static final int MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY = -1;
//在Trigger中默认选择MISFIRE_INSTRUCTION_FIRE_ONCE_NOW 策略
public static final int MISFIRE_INSTRUCTION_SMART_POLICY = 0;
// CornTrigger默认策略,合并部分misfire,正常执行下一个周期的任务。
public static final int MISFIRE_INSTRUCTION_FIRE_ONCE_NOW = 1;
//所有的misFire都不管,执行下一个周期的任务。
public static final int MISFIRE_INSTRUCTION_DO_NOTHING = 2;
  • 可以通过setMisfireInstruction方法设置misfire策略。
 CronTriggerFactoryBean triggerFactoryBean = new CronTriggerFactoryBean();
 triggerFactoryBean.setName("corn_" + clazzName);
 triggerFactoryBean.setJobDetail(jobFactory.getObject());
 triggerFactoryBean.setCronExpression(quartzCorn);
 triggerFactoryBean.setGroup(QUARTZ_TRIGGER_GROUP);
 //设置misfire策略
 triggerFactoryBean.setMisfireInstruction(CronTrigger.MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY);
 triggerFactoryBean.afterPropertiesSet();
  • 也可以通过CronScheduleBuilder设置misfire策略。
 CronScheduleBuilder csb = CronScheduleBuilder.cronSchedule("0/5 * * * * ?");
 //MISFIRE_INSTRUCTION_DO_NOTHING
 csb.withMisfireHandlingInstructionDoNothing();
 //MISFIRE_INSTRUCTION_FIRE_ONCE_NOW
 csb.withMisfireHandlingInstructionFireAndProceed();//(默认)
 //MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY
 csb.withMisfireHandlingInstructionIgnoreMisfires();

策略的具体含义

前提:在每个星期周一下午5点到7点,每隔一个小时执行一次,理论上共执行3次。

1. 情景一:

//所有misfire的任务会马上执行
public static final int MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY = -1;

若是5点misfire,6:15系统恢复之后,5,6点的misfire会马上执行。

2. 情景二:

//不做任何事情
public static final int MISFIRE_INSTRUCTION_DO_NOTHING = 2;

若是5点misfire,6:15系统恢复之后,只会执行7点的misfire。如果下次执行时间超过了end time,实际上就没有执行机会了。

3. 情景三:(cronTrigger的默认策略)

// CornTrigger默认策略,合并部分misfire,正常执行下一个周期的任务。
public static final int MISFIRE_INSTRUCTION_FIRE_ONCE_NOW = 1;

若是5点misfire,6:15系统恢复之后,立刻执行一次(只会一次)misfire。

2.2 quartz中SImpleTrigger使用的策略

  1. 若是使用MISFIRE_INSTRUCTION_SMART_POLICY—机智的策略
    • 如果Repeat=0; 【重复0次】
      instruction selected = MISFIRE_INSTRUCTION_FIRE_NOW;【立刻执行,对于不会重复执行的任务,只是默认的处理策略。】
    • 如果Repeat Count=REPEAT_INDEFINITELY;【无限重复】
      instruction selected = MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT;【在下一个激活点执行,且错过的执行机会作废。】
    • 如果Repeat Count>0;【有限重复】
      instruction selected = MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT;【立即执行,并执行指定的次数。】

3. misfire的执行流程

  1. 若配置(默认为true,可配置)成获取锁前先检查是否有需要recovery的trigger,先获取misfireCount;
  2. 获取TRIGGER_ACCESS锁;
  3. misfired的判断依据:status=waiting,current_time-next_fire_time>misfireThreshold(可配置,默认1分钟)【即实际触发时间-预计触发时间大于容忍度时间】,获取misfired的trigger,默认一个事务中只能最大有20个misfired trigger(可配置)。
  4. updateAfterMisfired:获取misfired的策略(默认是MISFIRE_INSTRUCTION_SMART_POLICY该策略在CronTrigger中为MISFIRE_INSTRUCTION_FIRE_ONCE_NOW),根据策略更新nexFireTime。
  5. 将nextFireTime等更新到trigger表;
  6. commit connection,释放锁。

image-20220827143045217

五、 有状态的job和无状态job

Job中有一个StatefulJob子接口,代表着有状态的任务,该接口是一个没有方法的标签接口,其目的就是让Quartz知道任务的类型,以便采用不同的执行方案。

 /* 
 *
 * @deprecated use DisallowConcurrentExecution and/or PersistJobDataAfterExecution annotations instead.
 * 
 * @author James House
 */
@PersistJobDataAfterExecution
@DisallowConcurrentExecution
public interface StatefulJob extends Job {

    /*
     * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     * 
     * Interface.
     * 
     * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     */

}

正如原码中描述:use DisallowConcurrentExecution and/or PersistJobDataAfterExecution annotations instead.该接口已过时,但是可以使用上面的注解代替。

  • @DisallowConcurrentExecution 不允许并发执行,即JOB为串行执行。
  • @PersistJobDataAfterExecution 在执行后将JobData持久化。

无状态任务在执行时,拥有自己的JobDataMap拷贝,对JobData的更改不会影响下次的执行。而有状态任务共享同一个JobDataMap实例,每次任务执行对JobDataMap所做的更改都会保存下来,后面的执行可以看到这个更改。也就是每次执行任务后都会对后面的执行发生影响。

正因为这个原因,无状态的Job可以并发执行,而有状态的StatefulJob不能并发执行,这意味着如果前次的StatefulJob还没有执行完毕,下一次的任务将阻塞等待,直到前次任务执行完毕。有状态任务比无状态任务需要考虑更多的因素,程序往往拥有更高的复杂度,因此除非必要,应该尽量使用无状态的Job。

如果Quartz使用了数据库持久化任务调度信息,无状态的JobDataMap仅会在Scheduler注册任务时保持一次,而有状态任务对应的JobDataMap在每次执行任务后都会进行保存。

Trigger自身也可以拥有一个JobDataMap,其关联的Job可以通过JobExecutionContext#getTrigger().getJobDataMap()获取Trigger中的JobDataMap。不管是有状态还是无状态的任务,在任务执行期间对Trigger的JobDataMap所做的更改都不会进行持久,也即不会对下次的执行产生影响。

案例测试:

1. 有状态的Job

@Component
public class TestJob2 extends CustomQuartzJobBean implements StatefulJob {
    private Logger logger = LoggerFactory.getLogger(TestJob2.class);

    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
        String value = jobDataMap.getString("key");
        logger.info("value的值:"+value);
        //设置jobData的值
        jobDataMap.put("key",value + "哈哈");

        logger.info("【数据库配置定时】-【结束】");
    }
}

执行结果:

2019-07-08 14:32:20,572 INFO [6105] [clusteredScheduler_Worker-6] [] (TestJob2.java:30): value的值:这是一个
2019-07-08 14:32:20,573 INFO [6106] [clusteredScheduler_Worker-6] [] (TestJob2.java:33): 【数据库配置定时】-【结束】
2019-07-08 14:32:25,020 INFO [10553] [clusteredScheduler_Worker-7] [] (TestJob2.java:30): value的值:这是一个哈哈
2019-07-08 14:32:25,021 INFO [10554] [clusteredScheduler_Worker-7] [] (TestJob2.java:33): 【数据库配置定时】-【结束】
2019-07-08 14:32:30,013 INFO [15546] [clusteredScheduler_Worker-8] [] (TestJob2.java:30): value的值:这是一个哈哈哈哈
2019-07-08 14:32:30,013 INFO [15546] [clusteredScheduler_Worker-8] [] (TestJob2.java:33): 【数据库配置定时】-【结束】
2019-07-08 14:32:35,017 INFO [20550] [clusteredScheduler_Worker-9] [] (TestJob2.java:30): value的值:这是一个哈哈哈哈哈哈
2019-07-08 14:32:35,018 INFO [20551] [clusteredScheduler_Worker-9] [] (TestJob2.java:33): 【数据库配置定时】-【结束】
2019-07-08 14:32:40,015 INFO [25548] [clusteredScheduler_Worker-10] [] (TestJob2.java:30): value的值:这是一个哈哈哈哈哈哈哈哈
2019-07-08 14:32:40,015 INFO [25548] [clusteredScheduler_Worker-10] [] (TestJob2.java:33): 【数据库配置定时】-【结束】
2019-07-08 14:32:45,016 INFO [30549] [clusteredScheduler_Worker-1] [] (TestJob2.java:30): value的值:这是一个哈哈哈哈哈哈哈哈哈哈
2019-07-08 14:32:45,016 INFO [30549] [clusteredScheduler_Worker-1] [] (TestJob2.java:33): 【数据库配置定时】-【结束】

2. 无状态的Job

@Component
public class TestJob2 extends CustomQuartzJobBean {
    private Logger logger = LoggerFactory.getLogger(TestJob2.class);

    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
        String value = jobDataMap.getString("key");
        logger.info("value的值:"+value);
        //设置jobData的值
        jobDataMap.put("key",value + "哈哈");

        logger.info("【数据库配置定时】-【结束】");
    }
}

执行结果:

2019-07-08 15:05:25,026 INFO [8174] [clusteredScheduler_Worker-2] [] (TestJob2.java:23): value的值:这是一个
2019-07-08 15:05:25,026 INFO [8174] [clusteredScheduler_Worker-2] [] (TestJob2.java:27): 【数据库配置定时】-【结束】
2019-07-08 15:05:30,017 INFO [13165] [clusteredScheduler_Worker-3] [] (TestJob2.java:23): value的值:这是一个
2019-07-08 15:05:30,017 INFO [13165] [clusteredScheduler_Worker-3] [] (TestJob2.java:27): 【数据库配置定时】-【结束】
2019-07-08 15:05:35,020 INFO [18168] [clusteredScheduler_Worker-4] [] (TestJob2.java:23): value的值:这是一个
2019-07-08 15:05:35,021 INFO [18169] [clusteredScheduler_Worker-4] [] (TestJob2.java:27): 【数据库配置定时】-【结束】
2019-07-08 15:05:40,018 INFO [23166] [clusteredScheduler_Worker-5] [] (TestJob2.java:23): value的值:这是一个
2019-07-08 15:05:40,019 INFO [23167] [clusteredScheduler_Worker-5] [] (TestJob2.java:27): 【数据库配置定时】-【结束】

六、 Trigger状态转换

在Quartz框架中,Trigger是一个重要的对象,定时任务的调度、触发都是通过Trigger的操作来实现的。

Trigger按照类型的不同,可以划分为SIMPLE、CORN、BLOB等类型,数据库中也有对应的表存储。除此之外QRTZ_TRIGGERSQRTZ_FIRED_TRIGGERS是两张存储Trigger调度的表。

正常获取、触发任务执行的流程:

一个触发器只能绑定一个Job,但是一个Job可以有多个触发器

调度器线程执行的时候,首先从triggers表中获取状态为WAITING,并且将要触发的Trigger。然后将WAITING状态更新为ACQUIRED,表示该触发器抢占到了,防止其他调度器(实例)抢占。然后插入触发器信息以及实例名到FRIED_TRIGGERS表中,状态为ACQUIRED。前面的更新和后面的插入是在一个事务中进行的。

该触发器抢占到任务后,等待触发时间的到来。

执行时间到来后,每触发一次任务,都会在FIRED_TRIGGERS表中创建一条记录,并且状态为EXECUTING。如果任务允许并发执行,此时TRIGGERS表里的状态更新为WAITING,PAUSED,COMPLETE(不需要执行)。

如果任务不允许并发执行,还会把Triggers表里的状态更新为BLOCK或PAUSED_BLOCK。

注意:Triggers表更新时根据 任务名和任务所属组名 而不是 触发器名称和触发器组名来更新的。这就解决了一个任务有多个触发器的并发问题;然后触发器线程会创建一个执行环境来执行任务,以便在任务执行完成后更新触发器的状态。任务执行完成后,在一个事务中触发器状态更新为WAITING,删除FIRED_TRIGGERS表里对应的记录。

image-20220827144342773

如何避免多个节点执行同一个任务

qrtz_trigger表中有NEXT_FIRE_TIME字段(下一次触发时间)。每个任务在即将执行的时候,获取qrtz_locks表中的行级锁,开启一个事务(更新qrtz_trigger表状态为ACQUIRED,并且插入qrtz_fire_trigger一条数据,起始状态为ACQUIRED)。

七、 Quartz集群原理

1. Quartz集群

img

Quartz集群

Quartz急群众每个节点是一个独立的Quartz任务应用,它又管理者其他节点。该集群需要分别对每个节点分别启动或停止,独立的Quartz节点并不与另一个节点或是管理节点通信。Quartz应用是通过共有相同数据库表来感知到另一应用。也就是说只有使用持久化JobStore存储Job和Trigger才能完成Quartz集群。

Quartz的集群部署方案是分布式的,没有负责集中管理的节点,而是利用数据库行锁的方式来实现集群环境下的并发控制。

一个scheduler实例在集群模式下首先获取{0}LOCKS表中的行锁;

向Mysql获取行锁的语句:

select * from {0}LOCKS where sched_name = ? and lock_name = ? for update

{0}会替换为配置文件默认配置的QRTZ_。sched_name为应用集群的实例名,lock_name就是行级锁名。Quartz主要由两个行级锁。

lock_name desc
STATE_ACCESS 状态访问锁
TRIGGER_ACCESS 触发器访问锁

Quartz集群争用触发器行锁,锁被占用只能等待,获取触发器行锁之后,先获取需要等待触发的其他触发器信息。数据库更新触发器状态信息,及时是否触发器行锁,供其他调度实例获取,然后在进行触发器任务调度操作,对数据库操作就要先获取行锁。

#集群中应用采用相同的Scheduler实例
org.quartz.scheduler.instanceName: wenqyScheduler

#集群节点的ID必须唯一,可由quartz自动生成
org.quartz.scheduler.instanceId: AUTO

#通知Scheduler实例要它参与到一个集群当中
org.quartz.jobStore.isClustered: true

#需持久化存储
org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate

#数据源
org.quartz.jobStore.dataSource=myDS

#quartz表前缀
org.quartz.jobStore.tablePrefix=QRTZ_

#数据源配置
org.quartz.dataSource.myDS.driver: com.mysql.jdbc.Driver
org.quartz.dataSource.myDS.URL: jdbc:mysql://localhost:3306/ncdb
org.quartz.dataSource.myDS.user: root
org.quartz.dataSource.myDS.password: 123456
org.quartz.dataSource.myDS.maxConnections: 5
org.quartz.dataSource.myDS.validationQuery: select 0

同一集群下,instanceName必须相同,instanceId可自动生成,isClustered为true,持久化存储,指定数据库类型对应的驱动类和数据源连接。

2. 集群故障转移

每个服务器会定时(org.quartz.jobStore.clusterCheckinInterval这个时间)更新SCHEDULER_STATE表中的LAST_CHECK_TIME(将服务器的当前时刻更新为最后更新时刻)字段,遍历集群各兄弟节点的实例状态,检测集群各个兄弟节点的健康状态。

2.1 如何检测故障节点

当集群的一个节点的Scheduler实例执行CHECKIN时,他会查看是否有其他节点Scheduler实例在到达他们预期的时间还未CHECKIN,则认为该节点故障。

LAST_CHECK_TIME+CHECKIN_INTERVAL<System.currentTimeMillis()

源码请参考org.quartz.impl.jdbcjobstore.JobStoreSupport下的

  /**
     * Get a list of all scheduler instances in the cluster that may have failed.
     * This includes this scheduler if it is checking in for the first time.
     */
    protected List<SchedulerStateRecord> findFailedInstances(Connection conn)
        throws JobPersistenceException {
    // find failed instances...
    if (calcFailedIfAfter(rec) < timeNow) {
    }            
protected long calcFailedIfAfter(SchedulerStateRecord rec) {
    return rec.getCheckinTimestamp() +
        Math.max(rec.getCheckinInterval(), 
                 (System.currentTimeMillis() - lastCheckin)) +
        7500L;
}

那么则认为该节点故障。

2.2 如何处理故障节点

集群管理线程检测到故障节点,就会更新触发器的状态,状态更新如下:

故障节点触发器更新前状态 更新后状态
BLOCK WAITING
PAUSED_BLOCK PAUSED
ACQUIRED WAITING
COMPLETE 无,删除Trigger

需要注意的是:若qrtz_fired_triggers不是ACQUIRED状态,而是执行状态,且jobRequestRecovery=true:

  • 创建一个SimpleTrigger,存储到triggers表中;
  • status=waiting,MISFIRE_INSTR=MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY。

然后删除firedTrigger。恢复故障的任务。

集群管理线程会删除故障节点的实例状态(qrtz_scheduler_state表),即重置了所有故障节点触发任务一般,原先故障任务和正常任务一样就交由了调度处理线程处理。

3. 注意问题

1. 时间同步问题

Quartz实际并不关心你是在相同还是不同的机器上运行节点。当集群放置在不同的机器上时,称之为水平集群。节点跑在同一台机器上时,称之为垂直集群。对于垂直集群,存在着单点故障的问题。这对高可用性的应用来说是无法接受的,因为一旦机器崩溃了,所有的节点也就被终止了。对于水平集群,存在着时间同步问题。

节点用时间戳来通知其他实例它自己的最后检入时间。假如节点的时钟被设置为将来的时间,那么运行中的Scheduler将再也意识不到那个结点已经宕掉了。另一方面,如果某个节点的时钟被设置为过去的时间,也许另一节点就会认定那个节点已宕掉并试图接过它的Job重运行。最简单的同步计算机时钟的方式是使用某一个Internet时间服务器(Internet Time Server ITS)。

2. 节点争抢Job问题

因为Quartz使用了一个随机的负载均衡算法,Job以随机的方式由不同的实例执行。Quartz官网上提到当前,还不存在一个方法来指派(钉住) 一个 Job 到集群中特定的节点。

八、 Quartz实现异步通知

使用Quartz如何实现梯度的异步通知呢?

1. 一个Job绑定多个触发器

一个Trigger只能绑定一个Job,但是一个Job可以绑定多个Trigger。

那么,若是我们将一个Job上绑定多个触发器,且每个触发器只是触发一次的话,那么,实际上我们便可以实现阶梯式的异步通知。

程序代码

public class TestQuartzForMoreJob {

    Logger logger=LoggerFactory.getLogger(TestQuartzForMoreJob.class);

    @Test
    public void test() {
        SimpleScheduleBuilder scheduleBuilder1=SimpleScheduleBuilder.repeatSecondlyForTotalCount(1);
        //任务
        JobDetail jobDetail = JobBuilder.newJob(HelloQuartz.class)
                .withIdentity("job1", "job_group1")
                .storeDurably()
                .build();

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        Date date = new Date();
        Date date1 = addSecond(date, 5);
        Date date2 = addSecond(date, 15);
        //日志打印
        logger.info("获取到任务的时间:"+sdf.format(date));
        logger.info("第一次通知的时间:"+sdf.format(date1));
        logger.info("第二次通知的时间:"+sdf.format(date2));
        //触发器1
        SimpleTrigger trigger = newTrigger().withIdentity("trigger1", "group1")
                .withSchedule(scheduleBuilder1)
                .startAt(date1)
                .forJob(new JobKey("job1", "job_group1"))
                .build();
        //触发器2
        SimpleTrigger trigger2 = newTrigger().withIdentity("trigger2", "group1")
                .withSchedule(scheduleBuilder1)
                .forJob(new JobKey("job1", "job_group1"))
                .startAt(date2)
                .build();
        try {
            Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
            //多触发器关联
            scheduler.scheduleJob(jobDetail, trigger);
            scheduler.scheduleJob(trigger2);
            scheduler.start();
            Thread.sleep(100000);
            scheduler.shutdown();
        } catch (SchedulerException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static Date addSecond(Date date, int second) {
        java.util.Calendar calendar = java.util.Calendar.getInstance();
        calendar.setTime(date);
//        calendar.add(Calendar.MINUTE, minute);
        calendar.add(Calendar.SECOND, second);
        return calendar.getTime();
    }
}

日志打印

17:19:53.215 [main] INFO com.com.tellme.TestQuartzForMoreJob - 获取到任务的时间:2019-07-09 05:19:53
17:19:53.220 [main] INFO com.com.tellme.TestQuartzForMoreJob - 第一次通知的时间:2019-07-09 05:19:58
17:19:53.220 [main] INFO com.com.tellme.TestQuartzForMoreJob - 第二次通知的时间:2019-07-09 05:20:08
17:19:58.218 [DefaultQuartzScheduler_Worker-1] INFO com.com.tellme.HelloQuartz - 多触发器测试,时间:2019-07-09 05:19:58
17:20:08.211 [DefaultQuartzScheduler_Worker-2] INFO com.com.tellme.HelloQuartz - 多触发器测试,时间:2019-07-09 05:20:08

2. 定时轮询数据库

这个思想是将需要异步通知的数据保存到数据表中,该表的结构有这么几个主要的字段:

CREATE TABLE `yy_notify` (
  `TXN_ID` varchar(32) NOT NULL COMMENT '系统单号',
  `SERVICE_ID` varchar(255) NOT NULL COMMENT 'Spring Bean id',
  `SCAN_STATUS` varchar(2) DEFAULT NULL COMMENT '扫描状态(00:待扫描,01:不扫描)',
  `SCAN_TIMES` int(1) DEFAULT '0' COMMENT '扫描次数',
  `NEXT_SCAN_TIME` timestamp NULL DEFAULT NULL COMMENT '下次扫描时间,用于梯度查询场景',
  `NOTIFY_STATUS` varchar(255) DEFAULT NULL COMMENT '(02-通知成功,03-通知返回失败,01-未通知)',
  `CREATE_TIME` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `MODIFY_TIME` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  PRIMARY KEY (`TXN_ID`) USING BTREE,
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

每1分钟轮询一次该表,将该表中NOTIFY_STATUS=未通知,SCAN_TIMES<=最大扫描次数,NEXT_SCAN_TIME<当前时间的数据取出,直接调用通知逻辑。

九、 动态操作Quartz定时任务

1. 如何创建一个任务

方法一:JobDetailFactoryBean或者CronTriggerFactoryBean

如何创建一个复杂的Bean对象,我们可以借助FactoryBean。而巧妙的是,JobDetailFactoryBean或者CronTriggerFactoryBean都实现了FactoryBean接口和InitializingBean接口。虽然我们new JobDetailFactoryBean(),但是实际上是将JobDetail交由的IOC管理。而InitializingBean接口会在属性装载完毕之后,自动的回调afterPropertiesSet()方法,完成Bean对象的最终构建:

@Bean
public JobDetailFactoryBean jobDetail(){
    //查询数据库或者配置文件
    JobDetailFactoryBean jobDetailFactoryBean=new JobDetailFactoryBean();
    jobDetailFactoryBean.setName("");
    jobDetailFactoryBean.setBeanName("");
    jobDetailFactoryBean.setJobClass((Class<? extends Job>) aClass);
    jobDetailFactoryBean.setGroup("");
    jobDetailFactoryBean.setDurability(true);
    return jobDetailFactoryBean;
}

而实际上更加直观的写法:

JobDetailFactoryBean jobFactory = new JobDetailFactoryBean();
jobFactory.setName("");
jobFactory.setBeanName("");
jobFactory.setJobClass((Class<? extends Job>) aClass);
jobFactory.setGroup("");
jobFactory.setDurability(true);
jobFactory.afterPropertiesSet();
//完成JobDetail的创建
JobDetail jobDetail= jobFactory.getObject();

方法二:使用建筑者模式创建Job或者Trigger

这种方式是通过建筑者模式创建Job或者Trigger。也是更加的优雅。

    //创建Job
    public static JobDetail getJobDetail(JobKey jobKey, String description, boolean jobShouldRecover, JobDataMap jobDataMap, Class<? extends Job> jobClass) {

        return JobBuilder.newJob(jobClass)
                .withIdentity(jobKey)
                .withDescription(description)
                .setJobData(jobDataMap)
                .usingJobData(jobDataMap)   //设置JobDataMap字段
                .requestRecovery(jobShouldRecover)
                .storeDurably()  //表示当没有触发器与之关联时,仍然将job继续保存在Scheduler中
                .build();
    }
    //创建Trigger
    public static Trigger getCornTrigger(TriggerKey triggerKey, String description, JobDataMap jobDataMap, String cronExpression, JobKey jobKey) {
        return TriggerBuilder.newTrigger()
                .withIdentity(triggerKey)
                .withDescription(description)
                .withSchedule(CronScheduleBuilder.cronSchedule(cronExpression))
                .forJob(jobKey.getName(), jobKey.getGroup()) //制定Trigger和Job的关联关系
                .usingJobData(jobDataMap)  //具体执行的方法中可以拿到这个传进去的信息。
                .build();
    }

2. 任务的动态操作

首先我们需要在项目启动时Spring容器启动完毕,去加载自定义定时配置表的配置,动态的去创建任务。那么需要实现CommandLineRunner/ApplicationRunner接口。
SpringBoot启动时初始化方法集合

实际上,动态操作定时任务,本质上就是操作scheduler(调度器)中的内容,所以实际上便是直接调用org.quartz.Scheduler类。然后根据自身业务进行扩展,代码就在附录中。

3. 注意事项

JobDataMap在Job和Trigger存储的数据并不一致。

@Slf4j
public class TestJob extends QuartzJobBean {

    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
        log.info("Job中的JobDataMap"+JSONObject.toJSONString(jobDataMap));
        JobDataMap jobDataMap1 = context.getTrigger().getJobDataMap();
        log.info("Trigger中的JobDataMap"+JSONObject.toJSONString(jobDataMap1));
        log.info("【定时任务测试--开始】");
    }
}

若是我们调用org.quartz.Scheduler中的triggerJob方法,立即执行一次定时任务,并且传入了JobDataMap,实际上context.getTrigger().getJobDataMap();才可以获取到。

 void triggerJob(JobKey jobKey, JobDataMap data)
        throws SchedulerException;

附录

动态操作定时的完整代码:

/**
 * Created by EalenXie on 2019/7/10 13:49.
 * 核心其实就是Scheduler的功能 , 这里只是非常简单的示例说明其功能
 * 如需根据自身业务进行扩展 请参考 {@link org.quartz.Scheduler}
 */
@Slf4j
@Service
public class QuartzJobService {

    //Quartz定时任务核心的功能实现类
    private Scheduler scheduler;

    public QuartzJobService(@Autowired SchedulerFactoryBean schedulerFactoryBean) {
        scheduler = schedulerFactoryBean.getScheduler();
    }

    /**
     * 创建和启动 定时任务
     * {@link org.quartz.Scheduler#scheduleJob(JobDetail, Trigger)}
     *
     * @param define 定时任务
     */
    public void scheduleJob(TaskDefine define) throws SchedulerException {
        //1.定时任务 的 名字和组名
        JobKey jobKey = define.getJobKey();
        //2.定时任务 的 元数据
        JobDataMap jobDataMap = getJobDataMap(define.getJobDataMap());
        //3.定时任务 的 描述
        String description = define.getDescription();
        //4.定时任务 的 逻辑实现类
        Class<? extends Job> jobClass = define.getJobClass();
        //5.定时任务 的 cron表达式
        String cron = define.getCronExpression();
        JobDetail jobDetail = getJobDetail(jobKey, description, jobDataMap, jobClass);
        Trigger trigger = getTrigger(jobKey, description, jobDataMap, cron);
        scheduler.scheduleJob(jobDetail, trigger);
    }


    /**
     * 暂停Job
     * {@link org.quartz.Scheduler#pauseJob(JobKey)}
     */
    public void pauseJob(JobKey jobKey) throws SchedulerException {
        scheduler.pauseJob(jobKey);
    }

    /**
     * 恢复Job
     * {@link org.quartz.Scheduler#resumeJob(JobKey)}
     */
    public void resumeJob(JobKey jobKey) throws SchedulerException {
        scheduler.resumeJob(jobKey);
    }

    /**
     * 删除Job
     * {@link org.quartz.Scheduler#deleteJob(JobKey)}
     */
    public void deleteJob(JobKey jobKey) throws SchedulerException {
        scheduler.deleteJob(jobKey);
    }


    /**
     * 修改Job 的cron表达式
     */
    public boolean modifyJobCron(TaskDefine define) {
        String cronExpression = define.getCronExpression();
        //1.如果cron表达式的格式不正确,则返回修改失败
        if (!CronExpression.isValidExpression(cronExpression)) return false;
        JobKey jobKey = define.getJobKey();
        TriggerKey triggerKey = new TriggerKey(jobKey.getName(), jobKey.getGroup());
        try {
            CronTrigger cronTrigger = (CronTrigger) scheduler.getTrigger(triggerKey);
            JobDataMap jobDataMap = getJobDataMap(define.getJobDataMap());
            //2.如果cron发生变化了,则按新cron触发 进行重新启动定时任务
            if (!cronTrigger.getCronExpression().equalsIgnoreCase(cronExpression)) {
                CronTrigger trigger = TriggerBuilder.newTrigger()
                    .withIdentity(triggerKey)
                    .withSchedule(CronScheduleBuilder.cronSchedule(cronExpression))
                    .usingJobData(jobDataMap)
                    .build();
                scheduler.rescheduleJob(triggerKey, trigger);
            }
        } catch (SchedulerException e) {
            log.error("printStackTrace", e);
            return false;
        }
        return true;
    }


    /**
     * 获取定时任务的定义
     * JobDetail是任务的定义,Job是任务的执行逻辑
     *
     * @param jobKey      定时任务的名称 组名
     * @param description 定时任务的 描述
     * @param jobDataMap  定时任务的 元数据
     * @param jobClass    {@link org.quartz.Job} 定时任务的 真正执行逻辑定义类
     */
    public JobDetail getJobDetail(JobKey jobKey, String description, JobDataMap jobDataMap, Class<? extends Job> jobClass) {
        return JobBuilder.newJob(jobClass)
            .withIdentity(jobKey)
            .withDescription(description)
            .setJobData(jobDataMap)
            .usingJobData(jobDataMap)
            .requestRecovery()
            .storeDurably()
            .build();
    }


    /**
     * 获取Trigger (Job的触发器,执行规则)
     *
     * @param jobKey         定时任务的名称 组名
     * @param description    定时任务的 描述
     * @param jobDataMap     定时任务的 元数据
     * @param cronExpression 定时任务的 执行cron表达式
     */
    public Trigger getTrigger(JobKey jobKey, String description, JobDataMap jobDataMap, String cronExpression) {
        return TriggerBuilder.newTrigger()
            .withIdentity(jobKey.getName(), jobKey.getGroup())
            .withDescription(description)
            .withSchedule(CronScheduleBuilder.cronSchedule(cronExpression))
            .usingJobData(jobDataMap)
            .build();
    }


    public JobDataMap getJobDataMap(Map<?, ?> map) {
        return map == null ? new JobDataMap() : new JobDataMap(map);
    }


}

文章参考

《SpringBoot整合Quartz作为调度中心使用完整实例》

十、监听

1. 概述

Quartz的监听器用于当任务调度中关注的事件发生时,能够及时获取这一事件的通知。

  1. Quartz监听的种类:
  • JobListener:任务监听;
  • TriggerListener:触发器监听;
  • SchedulerListener:调度器监听;
  1. 监听器的作用域
  • 全局监听器:能够接收到所有的Job/Trigger的事件通知;
  • 局部监听器:只能接收在其上注册Job或者Trigger的事件;

2. 监听器的种类

2.1 JobListener

源码位置:org.quartz.JobListener

public interface JobListener {
    //获取该JobListener的名称
    String getName();
    //Scheduler在JobDetail将要被执行时调用该方法
    void jobToBeExecuted(JobExecutionContext context);
    //Scheduler在JobDetail将要被执行时,但又被TriggerListener否决调用
    void jobExecutionVetoed(JobExecutionContext context);
    //任务执行完毕调用该方法
    void jobWasExecuted(JobExecutionContext context,
            JobExecutionException jobException);

}

将JobListener绑定到Scheduler中:

//监听所有的Job
scheduler.getListenerManager().addJobListener(new SimpleJobListener(), EverythingMatcher.allJobs());
//监听特定的Job
scheduler.getListenerManager().addJobListener(new SimpleJobListener(), KeyMatcher.keyEquals(JobKey.jobKey("HelloWorld1_Job", "HelloWorld1_Group")));
//监听同一任务组的Job
scheduler.getListenerManager().addJobListener(new SimpleJobListener(), GroupMatcher.jobGroupEquals("HelloWorld2_Group"));
//监听两个任务组的Job
scheduler.getListenerManager().addJobListener(new SimpleJobListener(), OrMatcher.or(GroupMatcher.jobGroupEquals("HelloWorld1_Group"), GroupMatcher.jobGroupEquals("HelloWorld2_Group")));

2.2 TriggerListener

源码位置:org.quartz.TriggerListener

触发器监听,即在任务调度过程中,与触发器Trigger相关的事件:触发器触发、触发器未正常触发、触发器触发完成等。

public interface TriggerListener {
    //获取触发器的名字
    public String getName();
    //Job的execute()方法被调用时调用该方法。
    public void triggerFired(Trigger trigger, JobExecutionContext context);
    //Trigger触发后,TriggerListener给了一个选择否定Job的执行。假如该方法返回true,该Job将不会被触发
    public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context);
    //Trigger错过触发时间触发该方法,此方法不应该含有长时间的处理逻辑。
    public void triggerMisfired(Trigger trigger);
    //Trigger被触发并且完成Job后触发。
    public void triggerComplete(Trigger trigger, JobExecutionContext context,
            int triggerInstructionCode);
}

将TriggerListener绑定到Scheduler中:

//监听所有的Trigger
scheduler.getListenerManager().addTriggerListener(new SimpleTriggerListener("SimpleTrigger"), EverythingMatcher.allTriggers());
//监听特定的Trigger
scheduler.getListenerManager().addTriggerListener(new SimpleTriggerListener("SimpleTrigger"), KeyMatcher.keyEquals(TriggerKey.triggerKey("HelloWord1_Job", "HelloWorld1_Group")));
//监听一组Trigger
scheduler.getListenerManager().addTriggerListener(new SimpleTriggerListener("SimpleTrigger"), GroupMatcher.groupEquals("HelloWorld1_Group"));
//移除监听器
scheduler.getListenerManager().removeTriggerListener("SimpleTrigger");

2.3 SchedulerListener

源码位置:org.quartz.SchedulerListener

SchedulerListener会在Scheduler的生命周期关键事件发生时调用。与Scheduler有关的事件包括:增加一个job/trigger,删除一个job/Trigger,scheduler发生严重错误,关闭Scheduler等。

public interface SchedulerListener {
     //用于部署JobDetail时调用
    public void jobScheduled(Trigger trigger);
    //用于卸载JobDetail时调用
    public void jobUnscheduled(String triggerName, String triggerGroup);
    //当一个Trigger没有触发次数时调用。
    public void triggerFinalized(Trigger trigger);

    public void triggersPaused(String triggerName, String triggerGroup);

    public void triggersResumed(String triggerName, String triggerGroup);
    //当一个或一组 JobDetail 暂停时调用这个方法。
    public void jobsPaused(String jobName, String jobGroup);
    //当一个或一组 Job 从暂停上恢复时调用这个方法。假如是一个 Job 组,jobName 参数将为 null。
    public void jobsResumed(String jobName, String jobGroup);
    //在 Scheduler 的正常运行期间产生一个严重错误时调用这个方法。
    public void schedulerError(String msg, SchedulerException cause);
    //当Scheduler 开启时,调用该方法
    public void schedulerStarted();
    //当Scheduler处于StandBy模式时,调用该方法
    public void schedulerInStandbyMode();
    //当Scheduler停止时,调用该方法
    public void schedulerShutdown();
    //当Scheduler中的数据被清除时,调用该方法。
    public void schedulingDataCleared();
}

将SchedulerListener绑定到Scheduler中:

//创建监听
scheduler.getListenerManager().addSchedulerListener(new SimpleSchedulerListener());
//移除监听
scheduler.getListenerManager().removeSchedulerListener(new SimpleSchedulerListener());

总结

以监听器的种类来说,比较有价值的便是

  1. org.quartz.TriggerListener#triggerMisfired,即一些比较重要的定时,若错过触发时间(并且超过了org.quartz.jobStore.misfireThreshold = 60000单位ms,忍受时间),就可以发送邮件报警。
  2. org.quartz.JobListener#jobWasExecuted,当定时任务中抛出异常后,该方法可以获取到该异常信息,然后进行报警出来(也可以进行立即重试或者移除所有触发器操作)。

Quartz使用(4) - Quartz监听器Listerner

十一、若依定时任务实战

1. 定时任务

在Web应用中,往往有一类功能是不可缺少的,那就是定时任务。 定时任务的应用非常广泛,如某些视频网站,每天会给会员送成长值,每月电影券; 比如在保证最终一致性的场景中,往往利用定时任务调度进行一些比对工作;比如一些定时需要生成的报表、邮件;比如一些需要定时清理数据的任务等。 在本项目中,提供方便友好的web界面,实现动态管理任务,可以达到动态控制定时任务启动、暂停、重启、删除、添加、修改等操作,极大地方便了开发过程

image-20220827195724504

1.1 属性说明

上图中,容易理解的就不介绍了,就讲讲比较重点的

1.1.1 调用方法

Bean调用示例:ryTask.ryParams('ry');Class类调用示例:com.ruoyi.quartz.task.RyTask.ryParams('ry');参数说明:支持字符串,布尔类型,长整型,浮点型,整型

  • 字符串(需要单引号''标识 如:ryTask.ryParams(’ry’))
  • 布尔类型(需要true false标识 如:ryTask.ryParams(true))
  • 长整型(需要L标识 如:ryTask.ryParams(2000L))
  • 浮点型(需要D标识 如:ryTask.ryParams(316.50D))
  • 整型(纯数字即可)

1.1.2 cron表达式语法

[秒] [分] [小时] [日] [月] [周] [年]

说明 必填 范围 通配符
0-59 , - * /
0-59 , - * /
小时 0-23 , - * /
1-31 , - * /
1-12 / JAN-DEC , - * ? / L W
1-7 or SUN-SAT , - * ? / L #
1970-2099 , - * /
通配符说明
  • * :所有值。 如:在分上设置 *,表每一分钟都会触发
  • ? :不指定值。使用的场景为不需要关心当前设置这个字段的值。如:每月10号触发一个操作,但不关心是周几,则需要周字段设置为”?” 具体设置为 0 0 0 10 * ?
  • - :区间。如在小时上设置 “10-12”,表 10,11,12点都会触发
  • , :指定多个值,如在周字段上设置 “MON,WED,FRI” 表示周一,周三和周五触发
  • / :用于递增触发。如在秒上面设置”5/15” 表示从5秒开始,每增15秒触发(5,20,35,50)。 在月字段上设置’1/3’所示每月1号开始,每隔三天触发一次
  • L :最后的意思。在日字段设置上,表当月最后一天(依据当前月份,如果是二月还会依据是否是润年[leap]), 在周字段上表示星期六,相当于”7”或”SAT”。如果在”L”前加上数字,则表示该数据的最后一个。例如在周字段上设置”6L”这样的格式,则表示“本月最后一个星期五”
  • W :离指定日期的最近那个工作日(周一至周五). 例如在日字段上置”15W”,表示离每月15号最近的那个工作日触发。如果15号正好是周六,则找最近的周五(14号)触发, 如果15号是周未,则找最近的下周一(16号)触发.如果15号正好在工作日(周一至周五),则就在该天触发。如果指定格式为 “1W”,它则表示每月1号往后最近的工作日触发。如果1号正是周六,则将在3号下周一触发。(注,”W”前只能设置具体的数字,不允许区间”-“)
  • # :序号(表示每月的第几个周几),例如在周字段上设置”6#3”表示在每月的第三个周六.注意如果指定”#5”,正好第五周没有周六,则不会触发该配置(用在母亲节和父亲节再合适不过了) ;小提示:’L’和 ‘W’可以一组合使用。如果在日字段上设置”LW”,则表示在本月的最后一个工作日触发;周字段的设置,若使用英文字母是不区分大小写的,即MON与mon相同
常用表达式例子
表达式 说明
0 0 2 1 * ? * 表示在每月的1日的凌晨2点调整任务
0 15 10 L * ? 每月最后一日的上午10:15触发
0 0 12 * * ? 每天中午12点触发
0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时
0 15 10 ? * MON-FRI 周一至周五的上午10:15触发

1.1.3 错误策略说明

  • 立即执行: 例如整点执行的任务,系统在九点死机了,在十点十五恢复(以下均是),则执行两次整点任务(九点和十点的)
  • 执行一次:只执行一次整点任务(十点的)
  • 放弃执行:不执行九点和十点的,等着做十一点的

1.2 定时任务调度测试

我们找到 quartz.task 包,在其中创建一个流量定时任务类 FlowTask ,如下

@Service("flow")
public class FlowTask {

    public void getFlowPackage(){
        System.out.println("查询所有流量包");
    }

    public void getFlowById(String orderId){
        System.out.println("查询流量:"+ orderId);
    }

    public void delFlowById(String orderId){
        System.out.println("删除流量:"+ orderId);
    }
}

然后启动项目,找到定时任务,创建一个查看全部流量包的定时任务,然后执行一次

image-20220827200438732

控制台输出

image-20220827200452145

我们试试查询指定订单的方法,注意方法参数用单引号包起来

image-20220827200503391

结果

image-20220827200511692

上面是用 Bean 方法来调用,我们也可以使用 Class 方式来调用,如 com.ruoyi.quartz.task.FlowTask.getFlowById('A2020123')

要开启定时任务,直接在状态属性部分点击开启,系统就会按照我们设置的cron表达式来执行;定时任务方法参数支持多参,字符串用 单引号 包起来,long 类型后面要加 L ,double 类型后面要加 D
删除也是同理,略''

1.3 定时任务代码详解

定时任务的数据库脚本存放在项目的sql文件夹的 quartz.sql 中,同时还要在quartz模块下添加依赖

<!-- 定时任务 -->
<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
    <exclusions>
        <exclusion>
            <groupId>com.mchange</groupId>
            <artifactId>c3p0</artifactId>
        </exclusion>
    </exclusions>
</dependency>

定时任务的配置在 quartz.config.ScheduleConfig ,大家自行看一下,配置完成后,项目启动便会执行初始化方法 quartz.service.impl.SysJobServiceImpl.init() ,主要是防止用户修改数据库sql文件,导致创建数据库时和代码不一致的问题;这个部分大家结合创建定时任务的界面内容来对比理解,实现步骤如下:

  1. 首先看到createScheduleJob() 方法,下面还有一个 getQuartzJobClass() 方法,这是用于控制是否并发,通过获取类名来判断是否是并发类(是并发,则类上用注解 @DisallowConcurrentExecution 来禁止并发)
  2. 然后将其添加到job对象中 JobBuilder.newJob(jobClass) ,再获取job的key(包括名称和分组) JobKey.jobKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup)
  3. 再创建cron表达式调度构建器和错误策略
  4. 创建trigger,表示示例创建好了
  5. 放入参数 jobDetail.getJobDataMap()
  6. 调用API判断,如果数据库已存在,先删除
  7. 调用API判断是否暂停 scheduler.pauseJob()

image-20220827200555228

接下来我们看一下运行流程,首先找到 quartz.controller.SysJobController.run() 方法,其中调用了一个 run() 的执行方法,我们找到其实现类 quartz.service.impl.SysJobServiceImpl.run()

@Override
@Transactional
public void run(SysJob job) throws SchedulerException
{
    Long jobId = job.getJobId();
    String jobGroup = job.getJobGroup();
    SysJob properties = selectJobById(job.getJobId());
    // 参数
    JobDataMap dataMap = new JobDataMap();
    dataMap.put(ScheduleConstants.TASK_PROPERTIES, properties);
    scheduler.triggerJob(ScheduleUtils.getJobKey(jobId, jobGroup), dataMap);
}

该方法传入一个job对象,调用 triggerJob() 方法来执行参数,其参数就是前面所创建的class(即实现过程1,对应数据库中qrtz_job_details表的job_class_name属性),我们看到开启并发类 QuartzJobExecution ,继承了 AbstractQuartzJob ,我们进去(可以看到继承了 Job 类),找到 execute() 方法,

@Override
public void execute(JobExecutionContext context) throws JobExecutionException
{
    SysJob sysJob = new SysJob();
    //获取数据目标key,其中存入了数据对象,保存了任务对象的信息,如id、名称等等,会保存到数据库的 sys_job 表和 sys_job_log 表      
    BeanUtils.copyBeanProp(sysJob, context.getMergedJobDataMap().get(ScheduleConstants.TASK_PROPERTIES));
    try
    {
        //通过before和after来记录时间,记录耗时
        before(context, sysJob);
        if (sysJob != null)
        {
            doExecute(context, sysJob);
        }
        //设置相关信息并写入数据库日志表 sys_job_log
        after(context, sysJob, null);
    }
    catch (Exception e)
    {
        log.error("任务执行异常  - :", e);
        after(context, sysJob, e);
    }
}

然后重写 doExecute() 方法,下面有两个子类,分别是 QuartzDisallowConcurrentExecutionQuartzJobExecution ,内容都是一样的,我们随便点进一个查看,可以看到有 JobInvokeUtil.invokeMethod(sysJob) 方法,用于执行具体目标类,我们进去看一下;

public static void invokeMethod(SysJob sysJob) throws Exception
{
    //获取bean和method名称,通过检测  .  来获取bean名称,通过检测  (  来获取方法名称
    String invokeTarget = sysJob.getInvokeTarget();
    String beanName = getBeanName(invokeTarget);
    String methodName = getMethodName(invokeTarget);
    //获取参数,getMethodParams() 对参数类型进行判断
    List<Object[]> methodParams = getMethodParams(invokeTarget);
    //通过 . 的数目来判断是bean还是class方式,最后都是执行invokeMethod方法来执行
    if (!isValidClassName(beanName))
    {
        Object bean = SpringUtils.getBean(beanName);
        invokeMethod(bean, methodName, methodParams);
    }
    else
    {
        Object bean = Class.forName(beanName).newInstance();
        invokeMethod(bean, methodName, methodParams);
    }

下面我们看 invokeMethod() 方法

if (StringUtils.isNotNull(methodParams) && methodParams.size() > 0)//带参
{
    //传入参数列表,再执行目标方法      
    Method method = bean.getClass().getDeclaredMethod(methodName, getMethodParamsType(methodParams));
    method.invoke(bean, getMethodParamsValue(methodParams));
}
else//无参
{
    //直接用反射机制执行目标方法
    Method method = bean.getClass().getDeclaredMethod(methodName);
    method.invoke(bean);
}

image-20220827200725683

posted @   llhhjj  阅读(341)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 如何给本地部署的DeepSeek投喂数据,让他更懂你
· 从 Windows Forms 到微服务的经验教训
· 李飞飞的50美金比肩DeepSeek把CEO忽悠瘸了,倒霉的却是程序员
· 超详细,DeepSeek 接入PyCharm实现AI编程!(支持本地部署DeepSeek及官方Dee
· 用 DeepSeek 给对象做个网站,她一定感动坏了
点击右上角即可分享
微信分享提示