目录
JDBC JobStore 持久化步骤概述
Spring Boot 集成 Quartz 定时器
Scheduer 调度器常用方法
JobDetal 与 Trigger 一对多
Quartz Scheduler 配置集群
1、本文环境:Spring boot 2.1.3 + quartz 2.3.0 + Mysql 驱动 8.0.15 + H2 驱动 1.4 + Java JDK 1.8。
(支持 mysql 数据库与 h2 数据库,如果没有安装 mysql 的,可以切换嵌入式的 h2 数据库)
本文源码:https://github.com/wangmaoxiong/quartzjdbc
1、《Quartz 数据持久化》默认使用 org.quartz.jobStore.class=org.quartz.simpl.RAMJobStore 内存存储,本文介绍 JobStoreTX jdbc 存储。
2、Quartz 调度信息可以通过 JDBC 保存到数据库中,支持主流关系型数据库,如:Oracle,PostgreSQL,MySQL,H2,SQL Server,HSQLDB 和 DB2 等。
3、实际生产中通常都是使用 JDBC 持久化,配置的调度信息都存储在数据中,应用服务器重启之后自动读取调度信息,然后继续按着规则进行自动执行,就像是一块钟表即使没电了,下次上了电池之后,仍然会自动运行。
一:创建数据库表
1、要使用 JDBC 持久化数据,首先必须创建一组数据库表以供 Quartz 使用,针对不同的数据库,org.quartz.impl.jdbcjobstore 包下提供了不同建表脚本。所有的表前缀都是 "QRTZ_",如 "QRTZ_TRIGGERS"、"QRTZ_JOB_DETAIL"。
2、执行程序之前,必须先手动执行脚本建表,否则启动时会报表不存在。而对于 h2 这种内存数据库,如果没有手动建表,则它会自动建表。
3、建表脚本传送,总共11张表,分别如下:
序号 | 表名 | 描述 |
---|---|---|
1 | qrtz_fired_triggers | 存储与已触发的 Trigger 相关的状态信息,以及相联 Job 的执行信息 |
2 | qrtz_paused_trigger_grps |
存储已暂停的 Trigger 相关信息 |
3 | qrtz_scheduler_state | 存储有关 Scheduler 的状态信息 |
4 | qrtz_locks | 存储程序锁信息 |
5 | qrtz_simple_triggers | 存储配置的 Simple Trigger 触发器信息 |
6 | qrtz_simprop_triggers | 存储即兴触发器信息 |
7 | qrtz_cron_triggers | 存储 Cron Trigger 触发器信息,如 Cron 表达式 |
8 | qrtz_blob_triggers | |
9 | qrtz_triggers | 存储已配置的 Trigger 的信息 |
10 | qrtz_job_details | 存储已配置的 Job 的详细信息 |
11 | qrtz_calendars | 存储 Quartz 的 Calendar 信息 |
二:确定事务管理类型
1、JobStoreTX:如果不需要将调度命令(例如添加和删除triggers)绑定到其他事务,那么可以通过使用 JobStoreTX 管理事务(这是最常见的选择)。
2、JobStoreCMT:如果需要 Quartz 与其他事务(即J2EE应用程序服务器)一起工作,那么应该使用 JobStoreCMT,这种情况下,Quartz 将让应用程序服务器容器管理事务。
#需要使用哪一种事务类型,配置文件中就指定谁
org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreCMT
三:设置数据源
1、quartz 的 JDBC 持久化需要从 DataSource 中获取与数据库的连接,DataSources 可以通过三种方式进行配置:
1)在 quartz 自己的配置文件 quartz.properties 中指定的所有池属性,以便 Quartz 可以自己创建 DataSource。w3c 教程、官网配置.
2)Quartz 直接使用应用服务器配置好的数据源。(本文使用方式)
3) 自定义的 org.quartz.utils.ConnectionProvider 实现
四:确定数据库驱动代理
1、需要为 JobStore 选择一个 DriverDelegate 才能使用,驱动代理负责执行特定数据库可能需要的任何 JDBC 工作。
2、针对不同的数据库制作了不同的数据库的代理,其中使用最多的是 StdJDBCDelegate ,它是一个使用 JDBC 代码(和SQL语句)来执行其工作的委托。其他驱动代理可以在 "org.quartz.impl.jdbcjobstore" 包或其子包中找到。如 DB2v6Delegate(用于DB2版本6及更早版本),HSQLDBDelegate(用于HSQLDB),MSSQLDelegate(SQLServer),PostgreSQLDelegate(用于PostgreSQL)),WeblogicDelegate(用于使用Weblogic创建的JDBC驱动程序)
3、配置文件中配置如下:
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
4、quartz.properties 可用属性的完整配置信息可以参考官网 Quartz Configuration,但也不是完全能用到那么多,下面将通过实际案例来进行一一使用。
1、通过封装公共的调度器业务层和控制层,以后需要新加功能时,则只需要新增 Job 实现即可,实现功能如下:
1)http://localhost:8080/schedule/findSchedulers?pageNum=1&pageSize=10:查询注册成功的作业信息
2)http://localhost:8080/schedule/scheduleJob:注册作业并启动
3)http://localhost:8080/schedule/rescheduleJob:重新注册任务的触发器
4)http://localhost:8080/schedule/deleteJob?jobName=xx&jobGroup=:删除指定调度作业
5)http://localhost:8080/schedule/pauseJob?jobName=xx&jobGroup=:暂停指定作业
6)http://localhost:8080/schedule/pauseAll:暂停所有作业
7)http://localhost:8080/schedule/resumeJob?jobName=xx&jobGroup=:恢复指定作业继续运行
8)http://localhost:8080/schedule/resumeAll:恢复所有作业
...
2、https://github.com/wangmaoxiong/quartzjdbc 源码中都有详细注释,所以为了不重复赘述,下面只提醒注意事项。
一:数据库建表
1、上面已经提到:执行程序之前,必须先手动执行脚本建表,否则启动时会报错表不存在。而对于 h2 这种内存数据库,如果没有手动建表,则它会自动建表。
2、建表脚本。表的含义查看上面。
二:pom.xml 依赖:https://github.com/wangmaoxiong/quartzjdbc/blob/master/pom.xml
1、导入了 mysql 数据库驱动和 h2 数据库驱动,如果没有安装 mysql 的,可以在配置文件中切换嵌入式的 h2 数据库.
2、spring-boot-starter-quartz 组件内部依赖了如下的组件:
ategory/License | Group / Artifact | Version |
---|---|---|
Job Scheduling/Apache 2.0 | org.quartz-scheduler » quartz | 2.3.2 |
Apache 2.0 | org.springframework » spring-context-support | 5.2.4.RELEASE |
Transactions/Apache 2.0 | org.springframework » spring-tx | 5.2.4.RELEASE |
Apache 2.0 | org.springframework.boot » spring-boot-starter | 2.2.5.RELEASE |
三:全局配置文件:https://github.com/wangmaoxiong/quartzjdbc/blob/master/src/main/resources/application.yml
1、如果没有安装 mysql 的,可以切换嵌入式的 h2 数据库:spring.profiles.active=h2DB
2、Spring Boot 对 Quartz Scheduler 集成之后,对它提供了属性配置,选项如下:
spring.quartz.auto-startup=true # 初始化后是否自动启动计划程序
spring.quartz.jdbc.comment-prefix=-- # SQL 初始化脚本中单行注释的前缀
spring.quartz.jdbc.initialize-schema=embedded # 数据库架构初始化模式
# 用于初始化数据库架构的SQL文件的路径
spring.quartz.jdbc.schema=classpath:org/quartz/impl/jdbcjobstore/tables_@@platform@@.sql
spring.quartz.job-store-type=memory # 石英调度器作业/任务存储类型
spring.quartz.overwrite-existing-jobs=false # 配置的作业是否应覆盖现有的作业定义
spring.quartz.properties.*= # 其他石英调度器属性,值是一个 Map
spring.quartz.scheduler-name=quartzScheduler # 计划程序的名称
spring.quartz.startup-delay=0s # 初始化完成后启动计划程序的延迟时间
spring.quartz.wait-for-jobs-to-complete-on-shutdown=false # 关闭时是否等待正在运行的作业完成
Spring boot 2.1.6 文档官网:common-application-properties 中可以查看到这些配置信息.
3、org.springframework.boot.autoconfigure.quartz.QuartzProperties 类专门映射 spring.quartz 开头的属性.
4、quartz 调度器自己的配置属性既可以配置在自己的 quartz.properties 配置文件中,也可以直接配置在 Spring boot 的 spring.quartz.properties 属性下,它是一个 Map
5、quartz 的所有配置都可以从官网 Quartz Configuration 获取.
四:BeanConfig 配置类:config/BeanConfig.java
1、Spring boot 源码 org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration 中可以看到应用启动时已经自动创建了 SchedulerFactoryBean 实例,所以需要注入它,然后 schedulerFactoryBean.getScheduler() 获取 Schduler。:
@Bean
@ConditionalOnMissingBean
public SchedulerFactoryBean quartzScheduler() {
SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
...
2、为了后期获取 Schduler,这里提供配置类将它提交给容器管理。同时提供了 RestTemplate 模板,方便后期做 http 请求.
五:页面返回值实体
1、ResultData 封装返回给页面的数据,由 code、message、data 三个属性组成,ResultCode 枚举定义常见的返回值状态.
java/com/wmx/quartzjdbc/pojo/ResultData.java
java/com/wmx/quartzjdbc/enums/ResultCode.java
六:调度信息实体:pojo/SchedulerEntity.java
1、为了方便注册调度任务与触发器,于是从 qrtz_job_details、qrtz_triggers 表中提取了一些常用字段出来组成实体,方便面向对象编程。
2、一是为了编码简单、二是考虑到 cron 触发器基本能满足开发需求,所以只操作 cron 触发器,其它触发器开发同理。
job_class_name:作业详情关联的 Job 类名,必须关联正确。
ron_expression: cron 触发器表达式,格式必须正确。在线Cron表达式生成器
七:quartz 作业/任务:jobs/RequestJob.java
1、执行定时任务逻辑的类。为了方便注入其它 service,或者其它组件,所以将类标识为 @Service 组件。
2、《Job/JobDetail 实例 与 并发》中说过 @DisallowConcurrentExecution、@PersistJobDataAfterExecution 注解用于处理高并发情况,当同一个 JobDetail 实例被并发执行时,由于竞争,JobDataMap 中存储的数据很可能是不确定的。
3、项目中以后新增任务功能时,只需要再实现 Job 接口编写业务逻辑即可。
八:调度业务层:service/SchedulerService.java
1、专门封装了 "启动、暂停、恢复、删除作业(Job)或者触发器",封装之后就通用了,所有的任务注册、暂停、删除等操作,都可以传入参数调用本类。以后如果在需要添加业务需求时,则只需要关心实现 Job 即可。
2、因为事先已经提供了 Scheduler ,所以现在只需要使用 @Resource 注入即可。
3、注册作业的时候注意下面两个参数:
3.1)storeDurably(boolean jobDurability):指示 job 是否是持久性的。如果 job 是非持久的,当没有活跃的 trigger 与之关联时,就会被自动地从 scheduler 中删除。即非持久的 job 的生命期是由 trigger 的存在与否决定的.
3.2)requestRecovery(boolean jobShouldRecover) :指示 job 遇到故障重启后,是否是可恢复的。如果 job 是可恢复的,在其执行的时候,如果 scheduler 发生硬关闭(hard shutdown)(比如运行的进程崩溃了,或者关机了),则当 scheduler 重启时,该 job 会被重新执行。
4、因为全局配置文件中配置了 spring.quartz.uto-startup=true,所以代码中不需要再手动启动:scheduler.start();
九:调度控制层:controller/SchedulerController.java
1、对外提供访问接口,通过约定参数进行注册任务、暂停、删除等操作。
2、http://localhost:8080/schedule/scheduleJob:注册作业并启动,post 提交的参数举例如下:
{
"job_name": "j2000",
"job_group": "reqJobGroup",
"job_class_name": "com.wmx.quartzjdbc.jobs.RequestJob",
"job_data": {
"url": "https://wangmaoxiong.blog.csdn.net/article/details/105080021"
},
"trigger_name": "t2000",
"trigger_group": "requestGroup",
"trigger_desc": "每1分钟访问一次",
"cron_expression": "0 0/1 * * * ?"
}
3、http://localhost:8080/schedule/rescheduleJob:重新注册任务的触发器,post 提交的参数举例如下:
{
"trigger_name": "t2000",
"trigger_group": "requestGroup",
"trigger_desc": "每1分钟访问一次",
"cron_expression": "0 0/1 * * * ?",
"trigger_data": {
"url": "https://wangmaoxiong.blog.csdn.net/article/details/105057405"
}
}
其它接口都亲测有效,可以自行测试。
1、综上所述,整个调度器关键的就是 Scheduler 对象,所以它的常用 API 方法汇总如下:
方法 | 描述 |
---|---|
addCalendar(String calName, Calendar calendar, boolean replace, boolean updateTriggers) | 向调度程序注册给定"日历",如果使用 jdbc 持久化,则对应 qrtz_calendars 表. 1)calName:假期日历的名称,触发器会根据名称进行引用、calendar:假期日历 2)replace:表示调度器 scheduler 中如果已经存在同名的日历是否替换 3)updateTriggers:是否更新引用了现有日历的现有触发器,以使其基于新触发器是"正确的". |
addJob(JobDetail jobDetail, boolean replace) | 将给定的作业添加到调度程序,此时未关联触发器,作业将"休眠"到它使用 Scheduler.triggerJob、或者 scheduler.scheduleJob 进行调度。 1)作业必须定义为"持久"(durability=true),即使没有关联触发器时,也不会被自动删除。 2)如果存在内部计划程序错误,或者如果作业不是持久,或已存在同名作业且 replace=false,都会抛出 抛出SchedulerException |
addJob(JobDetail jobDetail, boolean replace, boolean storeNonDurableWhileAwaitingScheduling) | storenondurablewileawitingscheduling 设置为 true,则可以存储非持久作业,一旦 Job 按计划开始执行,它将恢复正常的非持久行为(即一旦关联触发器的都结束时就会被删除)。 |
boolean checkExists(JobKey jobKey) | 根据 jobKey 检查对应的 Job 是否已经存在于计划程序中。如果存在具有给定标识符的作业,则返回 true。 |
boolean checkExists(TriggerKey triggerKey) | 根据 triggerKey 检查对应的 Trigger 是否已经存在于计划程序中。如果存在具有给定标识符的触发器,则返回 true。 |
clear() | 清除/删除所有计划数据,包括所有的 Job,所有的 Trigger,所有的 日历。如果 jdbc 持久化,则 clear 后,数据库相应表中的数据全部会被删除。可以查看源码:org.quartz.impl.jdbcjobstore.StdJDBCDelegate#clearData |
deleteCalendar(String calName) | 根据名称从调度程序中删除指定的日历。如果找到并删除了日历,则返回 true。 如果存在内部调度程序错误,或一个或多个触发器引用了被删除的日历,则抛 SchedulerException。 |
boolean deleteJob(JobKey jobKey) | 根据 JokKey 从调度程序中删除指定的作业及其关联的触发器。 如果找到并删除作业,则返回 true,如果存在内部计划程序错误,则抛出SchedulerException |
deleteJobs(List |
批量删除作业及其关联的触发器。如果找到并删除了所有作业,则返回 true;如果一个或多个未删除,则返回 false; |
Calendar getCalendar(String calName) | 根据名称获取调度器中注册好的日历 |
List |
获取所有注册了的 Calendar 名称 |
SchedulerContext getContext() | 返回调度程序的SchedulerContext。 |
List |
返回此计划程序实例中当前正在执行的所有作业。此方法不支持群集,只返回当前工作岗位,而不是在整个集群。 注意返回的列表是一个"瞬时"快照,并且一旦返回,执行作业的真正列表可能会有所不同。 |
JobDetail getJobDetail(JobKey jobKey) | 根据 jobKet 获取作业详情 JobDetail,返回的 JobDetail 对象是实际存储的快照。 如果要修改作业详细信息,则必须重新存储,如 addJob(JobDetail,boolean)。 |
List |
获取所有已知的 jobdail 组名称. |
Set |
使用 GroupMatcher 获取匹配的 jobKey 。 |
Set |
获取所有暂停的 Trigger 组的名称。 |
String getSchedulerName() | 返回调度程序的名称 |
String getSchedulerInstanceId() | 返回调度程序的实例Id |
Trigger getTrigger(TriggerKey triggerKey) | 使用给定的键获取 Trigger 实例,返回的触发器对象是实际存储的。如果要修改触发器,必须重新存储之后再触发(如 rescheduleejob(TriggerKey,trigger)。 |
List |
获取所有已知的 Trigger 组的名称. |
Set |
根据 GroupMatcher 获取匹配的触发器集合. |
List extends Trigger> getTriggersOfJob(JobKey jobKey) | 根据 jobKey 获取其关联的触发器列表。因为job与trigger是一对多. |
TriggerState getTriggerState(TriggerKey triggerKey) | 触发器的状态,如:PAUSED(暂停)、ACQUIRED(活动)、WAITING(等待) |
pauseAll() | 暂停所有触发器. 新触发器将在添加时暂停。 |
pauseJob(JobKey jobKey) | 暂停指定作业下的所有触发器. |
pauseJobs(GroupMatcher |
暂停 GroupMatcher 匹配到的所有作业下的所有触发器 |
pauseTrigger(TriggerKey triggerKey) | 暂停指定的触发器. |
pauseTriggers(GroupMatcher |
暂停 GroupMatcher 匹配到的多个触发器. |
Date rescheduleJob(TriggerKey triggerKey, Trigger newTrigger) | 重新注册触发器。先根据 triggerKey 删除指定的触发器,然后存储新触发器(newTrigger),并关联相同的作业. |
resumeAll() | 恢复(取消暂停)所有触发器。如果有触发器因为暂停错过一次或多次触发,则过期执行策略将被应用。 |
resumeJob(JobKey jobKey) | 恢复(取消暂停)指定作业下的所有触发器. |
resumeJobs(GroupMatcher |
批量恢复(取消暂停)作业. |
resumeTrigger(TriggerKey triggerKey) | 恢复(取消暂停)指定触发器 |
resumeTriggers(GroupMatcher |
批量恢复(取消暂停)触发器. |
Date scheduleJob(Trigger trigger) | 注册/调度给定的 Trigger,注意触发器必须正确关联已经存在的作业(Job),如使用 forJob(JobDetail jobDetail) 如果 trigger 未关联作业,或者 job 不存在,则抛 SchedulerException. 如果触发器不能添加到计划程序,或者有内部错误,则抛 SchedulerException. 如果注册的触发器已经存在,则抛异常,此时建议使用 rescheduleJob 修改触发器. |
Date scheduleJob(JobDetail jobDetail, Trigger trigger) | 将给定的 jobdail 注册/添加到调度程序,同时将给定的 Trigger 与之关联. 1)如果 trigger 已经引用(forJob(JobDetail jobDetail))了 jobDetail 之外的作业,则抛 SchedulerException. 2)如果 jobDetail、trigger 已经存在同名的 group 与 name,则也会抛异常. |
scheduleJob(JobDetail jobDetail, Set extends Trigger> triggersForJob, boolean replace) | 单个作业关联多个触发器注册 replace:如果注册的 jobDetail、trigger 已经存在,则更新它们. |
scheduleJobs(Map |
批量注册 JobDetail 与 triggersAndJobs,即一个作业对应多个触发器. replace:表示如果当存在同组同名的作业或者触发器时,是否更新它们。如果为 false,则必须保证添加的作业和触发器唯一,否则抛异常. |
shutdown() | 停止/关闭 quartz 调度程序,关闭了整个调度的线程池,意味者所有作业都不会继续执行。相当于 shutdown(false)。关闭后无法重新启动计划程序,只能重启应用 |
shutdown(boolean waitForJobsToComplete) | 参数表示是否如果当时作业正在执行,是否等待它执行完毕。关闭后无法重新启动计划程序,只能重启应用 |
start() | 启动触发 Trigger 的调度程序的线程,如果是首次呼叫,将启动失火/恢复过程。 如果 start 前已经调用了shutdown() 方法则抛出 SchedulerException. |
startDelayed(int seconds) | 延迟 seconds 秒后再启动,此方法不阻塞. |
boolean unscheduleJob(TriggerKey triggerKey) | 从调度程序中删除指定的 Trigger,如果相关关联的作业没有任何其他触发器,并且该作业是不持久的,则作业也将被删除。 |
boolean unscheduleJobs(List |
批量删除所有指定的触发器. |
isShutdown() | 报告 调度程序 是否已关闭。 |
1、quartz 设计的 Job、Trigger、Calendar 是相互独立的。
2、Job 与 trigger 是一对多:qrtz_job_details 表中有外键关联 qrtz_job_details 表中的 sched_name, job_name, job_group。
3、Job 被创建后,可以保存在 Scheduler 中,与 Trigger 是独立的,一个 Job 可以有多个 Trigger;这种松耦合的一个好处是可以修改或者替换 Trigger,而不用重新定义与之关联的 Job。
4、Calendar 通过 addCalendar 方法注册到 scheduler,触发器再通过 modifiedByCalendar(String calendarName)关联日历,同一个 Calendar 实例可用于多个 trigger。
5、主要是下面三个方法:
addJob(JobDetail jobDetail, boolean replace):注册作业.
scheduleJob(Trigger trigger):注册触发器,触发器中使用 triggerBuilder.forJob 方法先关联作业.
rescheduleJob(TriggerKey triggerKey, Trigger newTrigger):修改触发器,使用新触发器更新已存在的旧触发器.
/**
* 注册 job 与 触发器。区别于上面的是这里会对 作业和触发器进行分开注册.
* job_class_name 不能为空时,注册 JobDetail 作业详情,如果已经存在,则更新.
* cron_expression 不为空时,注册触发器(注册触发器时,对应的作业必须先存在):
* 根据参数 job_name、job_group 获取 JobDetail,如果存在,则关联此触发器与 JobDetail,然后注册触发器,
*
* @param schedulerEntity
* @return
*/
@PostMapping("schedule/scheduleJobOrTrigger")
public ResultData scheduleJobOrTrigger(@RequestBody SchedulerEntity schedulerEntity) {
ResultData resultData = null;
try {
schedulerService.scheduleJobOrTrigger(schedulerEntity);
resultData = new ResultData(ResultCode.SUCCESS, null);
} catch (Exception e) {
resultData = new ResultData(ResultCode.FAIL, null);
logger.error(e.getMessage(), e);
}
return resultData;
}
业务层实现如下:
/**
* 注册 job 与 触发器。区别于上面的是这里会对 作业和触发器进行分开注册.
* job_class_name 不能为空时,注册 JobDetail 作业详情,如果已经存在,则更新,不存在,则添加.
* cron_expression 不为空时,注册触发器(注册触发器时,对应的作业必须先存在):
* 根据参数 job_name、job_group 获取 JobDetail,如果存在,则关联此触发器与 JobDetail,然后注册触发器,
*
* @param schedulerEntity
* @throws SchedulerException
*/
public void scheduleJobOrTrigger(SchedulerEntity schedulerEntity) throws SchedulerException, ClassNotFoundException {
//1)注册 job 作业
String job_class_name = schedulerEntity.getJob_class_name();
JobDetail jobDetail = null;
if (StringUtils.isNotBlank(job_class_name)) {
jobDetail = this.getJobDetail(schedulerEntity);
//往调度器中添加作业.
scheduler.addJob(jobDetail, true);
logger.info("往调度器中添加作业 {}," + jobDetail.getKey());
}
//2)注册触发器,触发器必须关联已经存在的作业
String job_name = schedulerEntity.getJob_name();
String job_group = schedulerEntity.getJob_group();
if (jobDetail == null && StringUtils.isNotBlank(job_group) && StringUtils.isNotBlank(job_name)) {
jobDetail = scheduler.getJobDetail(JobKey.jobKey(job_name, job_group));
}
String cron_expression = schedulerEntity.getCron_expression();
Trigger trigger = null;
if (jobDetail != null && StringUtils.isNotBlank(cron_expression)) {
trigger = this.getTrigger(schedulerEntity, JobKey.jobKey(job_name, job_group));
}
if (trigger == null) {
return;
}
//注册触发器。如果触发器不存在,则新增,否则修改
boolean checkExists = scheduler.checkExists(trigger.getKey());
if (checkExists) {
//rescheduleJob(TriggerKey triggerKey, Trigger newTrigger):更新指定的触发器.
scheduler.rescheduleJob(trigger.getKey(), trigger);
} else {
//scheduleJob(Trigger trigger):注册触发器,如果触发器已经存在,则报错.
scheduler.scheduleJob(trigger);
}
}
/**
* 内部方法:处理 Trigger
* @param schedulerEntity
* @return
*/
private Trigger getTrigger(SchedulerEntity schedulerEntity, JobKey jobKey) {
//触发器参数
//schedulerEntity 中 job_data 属性值必须设置为 json 字符串格式,所以这里转为 JobDataMap 对象.
JobDataMap triggerDataMap = new JobDataMap();
Map triggerData = schedulerEntity.getTrigger_data();
if (triggerData != null && triggerData.size() > 0) {
triggerDataMap.putAll(triggerData);
}
//如果触发器名称为空,则使用 UUID 随机生成. group 为null时,会默认为 default.
if (StringUtils.isBlank(schedulerEntity.getTrigger_name())) {
schedulerEntity.setTrigger_name(UUID.randomUUID().toString());
}
//过期执行策略采用:MISFIRE_INSTRUCTION_DO_NOTHING
//forJob:为触发器关联作业. 一个触发器只能关联一个作业.
TriggerBuilder triggerBuilder = TriggerBuilder.newTrigger();
triggerBuilder.withIdentity(schedulerEntity.getTrigger_name(), schedulerEntity.getTrigger_group());
triggerBuilder.withDescription(schedulerEntity.getTrigger_desc());
triggerBuilder.usingJobData(triggerDataMap);
if (jobKey != null && jobKey.getName() != null) {
triggerBuilder.forJob(jobKey);
}
triggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(schedulerEntity.getCron_expression())
.withMisfireHandlingInstructionDoNothing());
return triggerBuilder.build();
}
/**
* 内部方法:处理 JobDetail
* storeDurably(boolean jobDurability):指示 job 是否是持久性的。如果 job 是非持久的,当没有活跃的 trigger 与之关联时,就会被自动地从 scheduler 中删除。即非持久的 job 的生命期是由 trigger 的存在与否决定的.
* requestRecovery(boolean jobShouldRecover) :指示 job 遇到故障重启后,是否是可恢复的。如果 job 是可恢复的,在其执行的时候,如果 scheduler 发生硬关闭(hard shutdown)(比如运行的进程崩溃了,或者关机了),则当 scheduler 重启时,该 job 会被重新执行。
*
* @param schedulerEntity
* @return
* @throws ClassNotFoundException
*/
private JobDetail getJobDetail(SchedulerEntity schedulerEntity) throws ClassNotFoundException {
//如果任务名称为空,则使用 UUID 随机生成.
if (StringUtils.isBlank(schedulerEntity.getJob_name())) {
schedulerEntity.setJob_name(UUID.randomUUID().toString());
}
Class extends Job> jobClass = (Class extends Job>) Class.forName(schedulerEntity.getJob_class_name());
//作业参数
JobDataMap jobDataMap = new JobDataMap();
Map jobData = schedulerEntity.getJob_data();
if (jobData != null && jobData.size() > 0) {
jobDataMap.putAll(jobData);
}
//设置任务详情.
return JobBuilder.newJob(jobClass)
.withIdentity(schedulerEntity.getJob_name(), schedulerEntity.getJob_group())
.withDescription(schedulerEntity.getJob_desc())
.usingJobData(jobDataMap)
.storeDurably(true)
.requestRecovery(true)
.build();
}
src/main/java/com/wmx/quartzjdbc/controller/SchedulerController.java
src/main/java/com/wmx/quartzjdbc/service/SchedulerService.java
1、现在的应用通常都采用多实例部署,使用集群的方式减轻服务器压力,防止单台服务器宕机而导致服务不可用。Quartz 的集群功能通过故障转移和负载均衡功能为调度程序带来高可用性和可扩展性。
2、quartz 集群适用JDBC 持久化(JobStoreTX 或 JobStoreCMT),并且基本上都是通过集群的每个节点共享相同的数据库来工作,即大家访问同一个数据。
3、quartz 集群有自己的负载均衡策略,每个节点都尽可能快地触发作业,当 Triggers 的触发时间发生时,获取到它的第一个节点(通过在其上放置一锁定)是将触发它的节点。
4、当某个节点出现故障时,其他节点会检测到该状况并识别数据库中在故障节点内正在进行的作业,任何标记为恢复的作业(requestRecovery=true)将被剩余的节点重新执行。没有标记为恢复的作业将在下一次相关的 Triggers 触发时执行。
5、集群功能最适合扩展长时间运行、或 cpu 密集型作业,通过多个节点分配工作负载,减轻服务器压力。
6、配置集群只需在非集群的基础上加上如下两项配置即可:
org.quartz.jobStore.isClustered=true #为 true 表示开启集群功能,默认为 false
org.quartz.scheduler.instanceId=auto #集群的每个节点名称必须唯一,可以手动指定,也可设置为 "AUTO" 此时自动命名.
7、官方建议集群下的各个实例应该使用相同的 quartz.properties 文件,也就是使用相同的 quartz 配置,instanceId 配置项除外。quartz 官网集群配置
8、下面测试 quartz 集群,使用 IDEA 将同一个应用并行启动,即启动一次后,修改服务器端口,然后再继续启动:
配置文件源码:https://github.com/wangmaoxiong/quartzjdbc/tree/master/src/main/resources
application.yml 非集群配置
application-cluster.yml 集群配置,使用时改下文件名称。
测试结果显示:2个 quartz 实例同时启动,当触发任务时,只会有一个实例执行作业,其它实例不会执行;当其中一个宕机时,另一个会继续执行调度。
十:后记:
1、至此文章结束,github 源码中都有详细注释。作为后台开发,专注于提供规范的接口即可,所以示例中并未提供操作页面。
2、通过封装调度控制层与业务层之后,后续新增任务执行逻辑时,则只需要添加 Job 实现类即可,然后通过接口注册启动。
3、因为 qurtz 自身已经提供了 11 张表,所以本文直接提取了表中的字段封装为实体对象,没有再新建自己的表,生产中根据实际情况进行设计。