一、Quartz入门
参考:
https://juejin.cn/post/7216679822097252411?searchId=20230726145213061AD6F989D36601FB8B
Quartz 是什么?
Quartz 是一款开源的任务调度框架,可以用于创建简单或复杂的任务调度,以执行数十、数百甚至上万个任务,这些任务被定义为标准 Java 组件,这些组件可以执行你想让他做的任何事情。Quartz 调度程序包括许多企业级特性,例如支持 JTA 事务(Java Transaction API,简写 JTA) 和集群。
我们知道在 SpringBoot
中有 @Schedule
注解可以用来实现对定时任务的增删改查,但是在某些场景下我们需要动态地修改定时任务,此时 @Schedule
注解很明显无法满足需求。
Quartz 的结构和特性
Quartz 的特性
- Quartz 可以嵌入到另一个独立的应用程序中运行;
- Quartz 可以在应用程序服务器中实例化并且参与分布式事务;
- Quartz 可以作为一个独立程序运行(比如说在 JVM中),我们同时可以通过 RMI(Remote Method Invocation)远程方法调用来使用它;
- Quartz 可以实例化为一个独立程序集群(具有负载均衡和容错性)。
Quartz 的组成
- 触发器(Trigger):当一个触发器触发时,Job 就会被调度执行,触发器就是用来定义何时触发,主要有四种 Trigger:
SimpleTrigger
、CronTrigger
、DataIntervalTrigger
、NthIncludedTrigger
; - 调度器(Scheduler):用于调度任务的执行,Scheduler 由工厂创建,主要包括
DirectSchedulerFactory
、StdSchedulerFactory
两种工厂,创建出的 Scheduler 主要包括RemoteMBeanScheduler
、RemoteScheduler
、StdScheduler
三种。 - 任务(Job):所有任务实例需要实现的接口,任务分为有状态任务和无状态任务,分别是
StateLessJob
、StateFullJob
; - 任务信息(JobDetail):所有任务的具体信息,例如任务携带的参数、任务的名称、任务组名称;
Quartz 任务执行过程
- 首先任务类都实现了
Job
接口; - 当触发器触发时调度器
Schduler
就会通知零个或多个实现了JobListener
或者TriggerListener
接口的对象; - 当任务执行完成,会返回一个状态码
JobCompletionCode
,我们可以根据状态码确定任务是否执行成功以及后续操作的执行; - 任务持久化,Quartz 的
JobStore
接口是任务存储接口,该接口的实现JDBCJobStore
可以将Job
、Trigger
持久化到数据库;接口实现RAMJobStore
可以将Job
、Trigger
存储到内存中;
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 的核心类,它实现了绑定JobDetail
、Trigger
,定时调度任务处理; - 创建触发器
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);
}
}
- 每次向调度器提交一个任务后,调度器就知道需要执行什么任务,只需要在构建
JobDetail
时提供任务的类型即可(HelloJob.Class
)。 - 在触发器被触发,调用
execute
方法前会创建该任务类的一个新实例,在执行完任务后对该实例的引用就会被丢弃,然后该实例将被垃圾回收掉。 JobDataMap
是用来保存任务执行时的数据及状态,JobDataMap
是 Java 中 Map 接口的一个实现,具有一些用于存储和检索原始类型数据的方法。如果任务在执行时需要一些数据,则可以在任务实例说明JobDetail
中添加相关参数,在任务调用execute()
执行时再从JobDetail
中获取到JobDataMap
来获取任务相关参数;- 不仅可以在
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
的构建算法抽离出来,交给TriggerBuilder
的 build()
方法去实现;然后具体的表现可以在生成时动态切换。
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_NOW
是 CronTrigger
的默认触发策略,MISFIRE_INSTRUCTION_SMART_POLICY
是 SimpleTrigger
使用的策略,具体细节如下:
MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY
:所有 misfire 任务立刻执行(比如说如果是 5:00 的misfire,6:15线程恢复后,5:00和6:00的 misfire 会立刻执行);MISFIRE_INSTRUCTION_FIRE_ONCE_NOW
:CornTrigger
的默认策略,合并部分misfire,正常执行下一个周期的任务(比如说如果是 5:00 的misfire,6:15线程恢复后只会立刻执行一次misfire);MISFIRE_INSTRUCTION_DO_NOTHING
:所有 misfire 都不管,执行下一个周期的任务(比如说 5:00 的misfire,6:15线程恢复后,只会执行7:00 的misfire);MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT
:立即执行,并执行指定次数;MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT
:立即执行,并且错过的机会作废;MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT
:在下一个激活点执行,并且错过的执行机会作废;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
接口分别创建了两个任务 FirstJob
、SecondJob
:
@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,控制台打印的任务执行情况也是串行执行:
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· 【.NET】调用本地 Deepseek 模型
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· 上周热点回顾(2.17-2.23)
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)