在日常项目研发中,定时任务可谓是必不可少的一环,关于 Spring Boot 如何实现静态定时任务、动态定时任务以及如何开启多线程跑任务,均已在上篇分享过,不再赘述。
虽然 Spring Boot 内置注解方式实现的定时任务,在一定程度上也能解决一定的业务场景问题,但是若做更复杂的动作,例如启停任务、删除任务等等操作,实现起来则稍显复杂,此时便可以通过集成开源任务框架来实现。
常见的定时任务框架有 Quartz、elastic-job、xxl-job等等,本次主要介绍 Spring Boot 集成定时任务第 3 部分:Spirng Boot 集成 Quartz 定时任务框架。
Quartz 存储方式有两种:MEMORY 和 JDBC。默认是内存形式维护任务信息,意味着服务重启了任务就从头再来,就像喝酒断片了一样;而 JDBC 形式就是能够把任务信息持久化到数据库,虽然服务重启了,依然还能接着来。
Quartz 提供了单机版和集群版,默认就是单机版,接下来逐一分享一波。
Spring Boot 集成 Quartz 的方式也很简单,首先引入封装好的 Quartz 依赖。
org.springframework.boot
spring-boot-starter-quartz
1. 内存方式存储任务信息
1.1 定义任务类
可以通过实现 Job 接口来定义任务,也可以通过继承 QuartzJobBean 这个抽象类来定义任务,其实 QuartzJobBean 本身也实现了 Job 接口,其本质都是实现 Job 接口来定义任务。
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;
/**
* 定义任务
*/
public class DongAoJob extends QuartzJobBean {
private static final Log logger = LogFactory.getLog(DongAoJob.class);
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
logger.info("幼年是盼盼,青年是晶晶,中年是冰墩墩,生活见好逐渐发福");
}
}
1.2 定义任务描述及任务触发规则
定义每隔 5 秒执行一次任务,代码如下。
import com.example.demo.quartz.task.DongAoJob;
import org.quartz.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 定义任务描述和具体的执行时间
*/
@Configuration
public class QuartzConfig {
@Bean
public JobDetail jobDetail() {
//指定任务描述具体的实现类
return JobBuilder.newJob(DongAoJob.class)
// 指定任务的名称
.withIdentity("dongAoJob")
// 任务描述
.withDescription("任务描述:用于输出冬奥欢迎语")
// 每次任务执行后进行存储
.storeDurably()
.build();
}
@Bean
public Trigger trigger() {
//创建触发器
return TriggerBuilder.newTrigger()
// 绑定工作任务
.forJob(jobDetail())
// 每隔 5 秒执行一次 job
.withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(5))
.build();
}
}
1.3 程序入口 main 函数
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoJobApplication {
public static void main(String[] args) {
SpringApplication.run(DemoJobApplication.class, args);
}
}
1.4 运行验证
通过控制台输出能够看到每 5 秒输出一次信息,达到预期效果。
通过控制台标注 1 的部分能够看到默认采用的是 RAMJobStore,就是将任务相关信息保存在内存里,应用重启后,定时任务信息将会丢失。
2. 数据库方式存储任务信息
2.1 引入依赖
在 pom.xml 文件中加入如下依赖信息。
com.mchange
c3p0
0.9.5.4
mysql
mysql-connector-java
2.2 添加 Quartz 配置信息
在 application.properties 文件中加入 Quartz 相关配置。
# 将 Quartz 持久化方式修改为 jdbc
spring.quartz.job-store-type=jdbc
# 实例名称(默认为quartzScheduler)
spring.quartz.properties.org.quartz.scheduler.instanceName=SC_Scheduler
# 实例节点 ID 自动生成
spring.quartz.properties.org.quartz.scheduler.instanceId=AUTO
# 修改存储内容使用的类
spring.quartz.properties.org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
# 数据源信息
spring.quartz.properties.org.quartz.jobStore.dataSource=quartz_jobs
spring.quartz.properties.org.quartz.dataSource.quartz_jobs.driver=com.mysql.cj.jdbc.Driver
spring.quartz.properties.org.quartz.dataSource.quartz_jobs.URL=jdbc:mysql://127.0.0.1:3306/quartz_jobs?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8
spring.quartz.properties.org.quartz.dataSource.quartz_jobs.user=root
spring.quartz.properties.org.quartz.dataSource.quartz_jobs.password=123456
2.3 初始化 Quartz 数据表信息
下载 Quartz 发布包,下载完成后,解压缩进入 quartz-2.2.3/docs/dbTables 目录,找到匹配数据库的 SQL 文件。
下载地址:
https://www.quartz-scheduler.org/downloads/files/quartz-2.2.3-distribution.tar.gz
为了方便演示,单独创建 quartz_jobs 数据库。
create database quartz_jobs default charset utf8;
use quartz_jobs;
然后在数据库中执行 tables-mysql.sql 初始化脚本,脚本内容如下(来源于官网压缩包),先别细究每个表的意义,用起来才是硬道理。
DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS;
DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE;
DROP TABLE IF EXISTS QRTZ_LOCKS;
DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_SIMPROP_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_JOB_DETAILS;
DROP TABLE IF EXISTS QRTZ_CALENDARS;
CREATE TABLE QRTZ_JOB_DETAILS
(
SCHED_NAME VARCHAR(120) NOT NULL,
JOB_NAME VARCHAR(200) NOT NULL,
JOB_GROUP VARCHAR(200) NOT NULL,
DESCRIPTION VARCHAR(250) NULL,
JOB_CLASS_NAME VARCHAR(250) NOT NULL,
IS_DURABLE VARCHAR(1) NOT NULL,
IS_NONCONCURRENT VARCHAR(1) NOT NULL,
IS_UPDATE_DATA VARCHAR(1) NOT NULL,
REQUESTS_RECOVERY VARCHAR(1) NOT NULL,
JOB_DATA BLOB NULL,
PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)
);
CREATE TABLE QRTZ_TRIGGERS
(
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
JOB_NAME VARCHAR(200) NOT NULL,
JOB_GROUP VARCHAR(200) NOT NULL,
DESCRIPTION VARCHAR(250) NULL,
NEXT_FIRE_TIME BIGINT(13) NULL,
PREV_FIRE_TIME BIGINT(13) NULL,
PRIORITY INTEGER NULL,
TRIGGER_STATE VARCHAR(16) NOT NULL,
TRIGGER_TYPE VARCHAR(8) NOT NULL,
START_TIME BIGINT(13) NOT NULL,
END_TIME BIGINT(13) NULL,
CALENDAR_NAME VARCHAR(200) NULL,
MISFIRE_INSTR SMALLINT(2) NULL,
JOB_DATA BLOB NULL,
PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)
REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP)
);
CREATE TABLE QRTZ_SIMPLE_TRIGGERS
(
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) 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),
FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);
CREATE TABLE QRTZ_CRON_TRIGGERS
(
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
CRON_EXPRESSION VARCHAR(200) NOT NULL,
TIME_ZONE_ID VARCHAR(80),
PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);
CREATE TABLE QRTZ_SIMPROP_TRIGGERS
(
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
STR_PROP_1 VARCHAR(512) NULL,
STR_PROP_2 VARCHAR(512) NULL,
STR_PROP_3 VARCHAR(512) NULL,
INT_PROP_1 INT NULL,
INT_PROP_2 INT NULL,
LONG_PROP_1 BIGINT NULL,
LONG_PROP_2 BIGINT NULL,
DEC_PROP_1 NUMERIC(13,4) NULL,
DEC_PROP_2 NUMERIC(13,4) NULL,
BOOL_PROP_1 VARCHAR(1) NULL,
BOOL_PROP_2 VARCHAR(1) NULL,
PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);
CREATE TABLE QRTZ_BLOB_TRIGGERS
(
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
BLOB_DATA BLOB NULL,
PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);
CREATE TABLE QRTZ_CALENDARS
(
SCHED_NAME VARCHAR(120) NOT NULL,
CALENDAR_NAME VARCHAR(200) NOT NULL,
CALENDAR BLOB NOT NULL,
PRIMARY KEY (SCHED_NAME,CALENDAR_NAME)
);
CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS
(
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP)
);
CREATE TABLE QRTZ_FIRED_TRIGGERS
(
SCHED_NAME VARCHAR(120) NOT NULL,
ENTRY_ID VARCHAR(95) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
INSTANCE_NAME VARCHAR(200) NOT NULL,
FIRED_TIME BIGINT(13) NOT NULL,
SCHED_TIME BIGINT(13) NOT NULL,
PRIORITY INTEGER NOT NULL,
STATE VARCHAR(16) NOT NULL,
JOB_NAME VARCHAR(200) NULL,
JOB_GROUP VARCHAR(200) NULL,
IS_NONCONCURRENT VARCHAR(1) NULL,
REQUESTS_RECOVERY VARCHAR(1) NULL,
PRIMARY KEY (SCHED_NAME,ENTRY_ID)
);
CREATE TABLE QRTZ_SCHEDULER_STATE
(
SCHED_NAME VARCHAR(120) NOT NULL,
INSTANCE_NAME VARCHAR(200) NOT NULL,
LAST_CHECKIN_TIME BIGINT(13) NOT NULL,
CHECKIN_INTERVAL BIGINT(13) NOT NULL,
PRIMARY KEY (SCHED_NAME,INSTANCE_NAME)
);
CREATE TABLE QRTZ_LOCKS
(
SCHED_NAME VARCHAR(120) NOT NULL,
LOCK_NAME VARCHAR(40) NOT NULL,
PRIMARY KEY (SCHED_NAME,LOCK_NAME)
);
commit;
2.4 运行验证
代码无需任何变动,直接运行 main 函数,控制台输出如下:
通过控制台输出发现任务正常跑起来,而且任务存储的方式变为了 jdbcjobstore.JobStoreTx,此时数据库表中也有任务信息插入了。
至此,Spring Boot 集成 Quartz 的两种存储任务方式就完事儿了,其实很简单,稍显复杂的多是 Quartz 相关的持久化依赖的 SQL 及配置信息。
但是,在某些业务场景下要求任务必须高可用、可扩展,那么单台服务器不能满足业务需求,这时就需要开启 Quartz 分布式定时任务啦。
3. 分布式任务支持
3.1 开启集群配置
在 application.properties 文件中,加入 Quartz 集群的配置信息。
# 开启集群,多个 Quartz 实例使用同一组数据库表
spring.quartz.properties.org.quartz.jobStore.isClustered=true
注意 Quartz 使用同一组数据库表作集群时,只需要配置相同的 instanceName 实例名称就可以,例如本次都用 SC_Scheduler。
spring.quartz.properties.org.quartz.scheduler.instanceName=SC_Scheduler
3.2 运行验证
为了方便启动多实例验证,把服务启动的端口进行随机生成,application.properties 文件加入配置如下。
server.port=${random.int[10000,19999]}
直接运行 main 函数,启动实例 1;再次运行 main 函数,启动实例2(如果 IDEA 不让重复运行,那就重新复制一个启动类,再运行)。
通过红色圈住部分 is clustered,显然已是开启了集群。若关闭其中一个正在跑任务的节点,观察另一个节点是否会自动检测继续执行任务呢?
停掉节点 1:
节点 2 继续接着执行任务:
至此,Spring Boot 集成 Quartz 可以接近尾声了。
不过稍显不甘心呢,因为回头看代码,会发现 QuartzConfig 中的 job 和 trigger 都是硬编码方式完成的。
这样肯定无法管理任务的状态,无法做到扩展,更达不到 Spring Boot 的思想让程序员更专注业务开发,所以仍有改进的空间,仍存很多疑问。
疑问:是否可以通过 API 动态创建任务呢?
疑问:是否可以通过 API 编辑任务的执行时间呢?
疑问:是否可以通过 API 暂停/恢复任务呢?
疑问:是否可以通过 API 删除任务呢?
疑问:是否可以 ... ...
此时,可以这么回答:of course.Im very sure. 老鼠拉木锨——大头在后面呢,不过限于自定义代码较多,单独放一代码篇去分享。
4. 例行回顾
本文是 Spring Boot 项目集成 Quartz 定时任务框架讲解,主要分享了如下部分:
默认内存方式存储任务信息;
数据库方式任务信息;
分布式任务支持
玩转 Spring Boot 集成 Quartz 定时任务就写到这里,下次一起编码实现 Quartz 任务的动态管理。
一起聊技术、谈业务、喷架构,少走弯路,不踩大坑,会持续输出更多精彩分享,欢迎关注,敬请期待!
历史系列文章:
玩转 Spring Boot 入门篇
玩转 Spring Boot 集成篇(MySQL、Druid、HikariCP)
玩转 Spring Boot 集成篇(MyBatis、JPA、事务支持)
玩转 Spring Boot 集成篇(Redis)
玩转 Spring Boot 集成篇(Actuator、Spring Boot Admin)
玩转 Spring Boot 集成篇(RabbitMQ)
玩转 Spring Boot 集成篇(@Scheduled、静态、动态定时任务)