示例代码对应仓库:lab-28-task-quartz-memory 。
实际场景下,我们必然需要考虑定时任务的高可用,所以基本上,肯定使用 Quartz 的集群方案。因此本小节,我们使用 Quartz 的 JDBC 存储器 JobStoreTX ,并是使用 MySQL 作为数据库。
如下是 Quartz 两种存储器的对比:
FROM https://blog.csdn.net/Evankaka/article/details/45540885
实际上,有方案可以实现兼具这两种方式的优点
另外,本小节提供的示例和 「3. 快速入门 Quartz 单机」 基本一致。😈 下面,让我们开始遨游~
引入依赖
在 pom.xml 文件中,引入相关依赖。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.10.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>lab-28-task-quartz-jdbc</artifactId>
<dependencies>
<!-- 实现对数据库连接池的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency> <!-- 本示例,我们使用 MySQL -->
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.48</version>
</dependency>
<!-- 实现对 Spring MVC 的自动化配置 -->
<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>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
和 「3.1 引入依赖」 基本一致,只是额外引入 spring-boot-starter-test 依赖,等会会写两个单元测试方法。
示例 Job
在 cn.iocoder.springboot.lab28.task.config.job 包路径下,创建 DemoJob01 和 DemoJob02 类。代码如下:
// DemoJob01.java
@DisallowConcurrentExecution
public class DemoJob01 extends QuartzJobBean {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private DemoService demoService;
@Override
protected void executeInternal(JobExecutionContext context) {
logger.info("[executeInternal][我开始的执行了, demoService 为 ({})]", demoService);
}
}
// DemoJob02.java
@DisallowConcurrentExecution
public class DemoJob02 extends QuartzJobBean {
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
protected void executeInternal(JobExecutionContext context) {
logger.info("[executeInternal][我开始的执行了]");
}
}
相比 「3.2 示例 Job」 来说,在类上添加了 Quartz 的 @DisallowConcurrentExecution 注解,保证相同 JobDetail 在多个 JVM 进程中,有且仅有一个节点在执行。
注意,不是以 Quartz Job 为维度,保证在多个 JVM 进程中,有且仅有一个节点在执行,而是以 JobDetail 为维度。虽然说,绝大多数情况下,我们会保证一个 Job 和 JobDetail 是一一对应。😈 所以,搞不清楚这个概念的胖友,最好搞清楚这个概念。实在有点懵逼,保证一个 Job 和 JobDetail 是一一对应就对了。
而 JobDetail 的唯一标识是 JobKey ,使用 name + group 两个属性。一般情况下,我们只需要设置 name 即可,而 Quartz 会默认 group = DEFAULT 。
不过这里还有一点要补充,也是需要注意的,在 Quartz 中,相同 Scheduler 名字的节点,形成一个 Quartz 集群。在下文中,我们可以通过 spring.quartz.scheduler-name 配置项,设置 Scheduler 的名字。
【重要】为什么要说这个呢?因为我们要完善一下上面的说法:通过在 Job 实现类上添加 @DisallowConcurrentExecution 注解,实现在相同 Quartz Scheduler 集群中,相同 JobKey 的 JobDetail ,保证在多个 JVM 进程中,有且仅有一个节点在执行。
应用配置文件
在 application.yml 中,添加 Quartz 的配置,如下:
spring:
datasource:
user:
url: jdbc:mysql://127.0.0.1:3306/lab-28-quartz-jdbc-user? useSSL=false&useUnicode=true&characterEncoding=UTF-8
driver-class-name: com.mysql.jdbc.Driver
username: root
password:
quartz:
url: jdbc:mysql://127.0.0.1:3306/lab-28-quartz-jdbc-quartz?useSSL=false&useUnicode=true&characterEncoding=UTF-8
driver-class-name: com.mysql.jdbc.Driver
username: root
password:
Quartz 的配置,对应 QuartzProperties 配置类
quartz:
scheduler-name: clusteredScheduler # Scheduler 名字。默认为 schedulerName
job-store-type: jdbc # Job 存储器类型。默认为 memory 表示内存,可选 jdbc 使用数据库。
auto-startup: true # Quartz 是否自动启动
startup-delay: 0 # 延迟 N 秒启动
wait-for-jobs-to-complete-on-shutdown: true # 应用关闭时,是否等待定时任务执行完成。默认为 false ,建议设置为 true
overwrite-existing-jobs: false # 是否覆盖已有 Job 的配置
properties: # 添加 Quartz Scheduler 附加属性,更多可以看 http://www.quartz- scheduler.org/documentation/2.4.0-SNAPSHOT/configuration.html 文档
org:
quartz:
# JobStore 相关配置
jobStore:
# 数据源名称
dataSource: quartzDataSource # 使用的数据源
class: org.quartz.impl.jdbcjobstore.JobStoreTX # JobStore 实现类
driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
tablePrefix: QRTZ_ # Quartz 表前缀
isClustered: true # 是集群模式
clusterCheckinInterval: 1000
useProperties: false
# 线程池相关配置
threadPool:
threadCount: 25 # 线程池大小。默认为 10 。
threadPriority: 5 # 线程优先级
class: org.quartz.simpl.SimpleThreadPool # 线程池类型
jdbc: # 使用 JDBC 的 JobStore 的时候,JDBC 的配置
initialize-schema: never # 是否自动使用 SQL 初始化 Quartz 表结构。这里设置成 never ,我们手动创建表结构。
配置项比较多,我们主要对比 「3.5 应用配置文件」 来看看。
在 spring.datasource 配置项下,用于创建多个数据源的配置。
user 配置,连接 lab-28-quartz-jdbc-user 库。目的是,为了模拟我们一般项目,使用到的业务数据库。
quartz 配置,连接 lab-28-quartz-jdbc-quartz 库。目的是,Quartz 会使用单独的数据库。😈 如果我们有多个项目需要使用到 Quartz 数据库的话,可以统一使用一个,但是要注意配置 spring.quartz.scheduler-name 设置不同的 Scheduler 名字,形成不同的 Quartz 集群。
在 spring.quartz 配置项下,额外增加了一些配置项,我们逐个来看看。
scheduler-name 配置,Scheduler 名字。这个我们在上文解释了很多次了,如果还不明白,请拍死自己。
job-store-type 配置,设置了使用 "jdbc" 的 Job 存储器。
properties.org.quartz.jobStore 配置,增加了 JobStore 相关配置。重点是,通过 dataSource 配置项,设置了使用名字为 "quartzDataSource" 的 DataSource 为数据源。😈 在 「4.4 DataSourceConfiguration」 中,我们会使用 spring.datasource.quartz 配置,来创建该数据源。
jdbc 配置项,虽然名字叫这个,主要是为了设置使用 SQL 初始化 Quartz 表结构。这里,我们设置 initialize-schema = never ,我们手动创建表结构。
咳咳咳,配置项确实有点多。如果暂时搞不明白的胖友,可以先简单把 spring.datasource 数据源,修改成自己的即可。
初始化 Quartz 表结构
在 Quartz Download 地址,下载对应版本的发布包。解压后,我们可以在 src/org/quartz/impl/jdbcjobstore/ 目录,看到各种数据库的 Quartz 表结构的初始化脚本。这里,因为我们使用 MySQL ,所以使用 tables_mysql_innodb.sql 脚本。
在数据库中执行该脚本,完成初始化 Quartz 表结构。如下图所示:
关于每个 Quartz 表结构的说明,可以看看 《Quartz 框架(二)——JobStore 数据库表字段详解》 文章。😈 实际上,也可以不看,哈哈哈哈。
我们会发现,每个表都有一个 SCHED_NAME 字段,Quartz Scheduler 名字。这样,实现每个 Quartz 集群,数据层面的拆分。
DataSourceConfiguration
在 cn.iocoder.springboot.lab28.task.config 包路径下,创建 DataSourceConfiguration 类,配置数据源。代码如下:
// DataSourceConfiguration.java
@Configuration
public class DataSourceConfiguration {
/**
* 创建 user 数据源的配置对象
*/
@Primary
@Bean(name = "userDataSourceProperties")
@ConfigurationProperties(prefix = "spring.datasource.user") // 读取 spring.datasource.user 配置到 DataSourceProperties 对象
public DataSourceProperties userDataSourceProperties() {
return new DataSourceProperties();
}
/**
* 创建 user 数据源
*/
@Primary
@Bean(name = "userDataSource")
@ConfigurationProperties(prefix = "spring.datasource.user.hikari") // 读取 spring.datasource.user 配置到 HikariDataSource 对象
public DataSource userDataSource() {
// 获得 DataSourceProperties 对象
DataSourceProperties properties = this.userDataSourceProperties();
// 创建 HikariDataSource 对象
return createHikariDataSource(properties);
}
/**
* 创建 quartz 数据源的配置对象
*/
@Bean(name = "quartzDataSourceProperties")
@ConfigurationProperties(prefix = "spring.datasource.quartz") // 读取 spring.datasource.quartz 配置到 DataSourceProperties 对象
public DataSourceProperties quartzDataSourceProperties() {
return new DataSourceProperties();
}
/**
* 创建 quartz 数据源
*/
@Bean(name = "quartzDataSource")
@ConfigurationProperties(prefix = "spring.datasource.quartz.hikari")
@QuartzDataSource
public DataSource quartzDataSource() {
// 获得 DataSourceProperties 对象
DataSourceProperties properties = this.quartzDataSourceProperties();
// 创建 HikariDataSource 对象
return createHikariDataSource(properties);
}
private static HikariDataSource createHikariDataSource(DataSourceProperties properties) {
// 创建 HikariDataSource 对象
HikariDataSource dataSource = properties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
// 设置线程池名
if (StringUtils.hasText(properties.getName())) {
dataSource.setPoolName(properties.getName());
}
return dataSource;
}
}
基于 spring.datasource.user 配置项,创建了名字为 "userDataSource" 的 DataSource Bean 。并且,在其上我们添加了 @Primay 注解,表示其是主数据源。
基于 spring.datasource.quartz 配置项,创建了名字为 "quartzDataSource" 的 DataSource Bean 。并且,在其上我们添加了 @QuartzDataSource 注解,表示其是 Quartz 的数据源。😈 注意,一定要配置啊,这里卡了好久!!!!
定时任务配置
完成上述的工作之后,我们需要配置 Quartz 的定时任务。目前,有两种方式:
方式一,「4.6.1 Bean 自动设置」 。
方式二,「4.6.2 Scheduler 手动设置」 。
Bean 自动设置
在 cn.iocoder.springboot.lab28.task.config 包路径下,创建 ScheduleConfiguration 类,配置上述的两个示例 Job 。代码如下:
// ScheduleConfiguration.java
@Configuration
public class ScheduleConfiguration {
public static class DemoJob01Configuration {
@Bean
public JobDetail demoJob01() {
return JobBuilder.newJob(DemoJob01.class)
.withIdentity("demoJob01") // 名字为 demoJob01
.storeDurably() // 没有 Trigger 关联的时候任务是否被保留。因为创建 JobDetail 时,还没 Trigger 指向它,所以需要设置为 true ,表示保留。
.build();
}
@Bean
public Trigger demoJob01Trigger() {
// 简单的调度计划的构造器
SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(5) // 频率。
.repeatForever(); // 次数。
// Trigger 构造器
return TriggerBuilder.newTrigger()
.forJob(demoJob01()) // 对应 Job 为 demoJob01
.withIdentity("demoJob01Trigger") // 名字为 demoJob01Trigger
.withSchedule(scheduleBuilder) // 对应 Schedule 为 scheduleBuilder
.build();
}
}
public static class DemoJob02Configuration {
@Bean
public JobDetail demoJob02() {
return JobBuilder.newJob(DemoJob02.class)
.withIdentity("demoJob02") // 名字为 demoJob02
.storeDurably() // 没有 Trigger 关联的时候任务是否被保留。因为创建 JobDetail 时,还没 Trigger 指向它,所以需要设置为 true ,表示保留。
.build();
}
@Bean
public Trigger demoJob02Trigger() {
// 简单的调度计划的构造器
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/10 * * * * ? *");
// Trigger 构造器
return TriggerBuilder.newTrigger()
.forJob(demoJob02()) // 对应 Job 为 demoJob02
.withIdentity("demoJob02Trigger") // 名字为 demoJob02Trigger
.withSchedule(scheduleBuilder) // 对应 Schedule 为 scheduleBuilder
.build();
}
}
}
和 「3.3 ScheduleConfiguration」 是一模一样的。
在 Quartz 调度器启动的时候,会根据该配置,自动调用如下方法:
Scheduler#addJob(JobDetail jobDetail, boolean replace) 方法,将 JobDetail 持久化到数据库。
Scheduler#scheduleJob(Trigger trigger) 方法,将 Trigger 持久化到数据库。
Scheduler 手动设置
一般情况下,推荐使用 Scheduler 手动设置。
创建 QuartzSchedulerTest 类,创建分别添加 DemoJob01 和 DemoJob02 的 Quartz 定时任务配置。代码如下:
// QuartzSchedulerTest.java
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class QuartzSchedulerTest {
@Autowired
private Scheduler scheduler;
@Test
public void addDemoJob01Config() throws SchedulerException {
// 创建 JobDetail
JobDetail jobDetail = JobBuilder.newJob(DemoJob01.class)
.withIdentity("demoJob01") // 名字为 demoJob01
.storeDurably() // 没有 Trigger 关联的时候任务是否被保留。因为创建 JobDetail 时,还没 Trigger 指向它,所以需要设置为 true ,表示保留。
.build();
// 创建 Trigger
SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(5) // 频率。
.repeatForever(); // 次数。
Trigger trigger = TriggerBuilder.newTrigger()
.forJob(jobDetail) // 对应 Job 为 demoJob01
.withIdentity("demoJob01Trigger") // 名字为 demoJob01Trigger
.withSchedule(scheduleBuilder) // 对应 Schedule 为 scheduleBuilder
.build();
// 添加调度任务
scheduler.scheduleJob(jobDetail, trigger);
// scheduler.scheduleJob(jobDetail, Sets.newSet(trigger), true);
}
@Test
public void addDemoJob02Config() throws SchedulerException {
// 创建 JobDetail
JobDetail jobDetail = JobBuilder.newJob(DemoJob02.class)
.withIdentity("demoJob02") // 名字为 demoJob02
.storeDurably() // 没有 Trigger 关联的时候任务是否被保留。因为创建 JobDetail 时, 还没 Trigger 指向它,所以需要设置为 true ,表示保留。
.build();
// 创建 Trigger
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/10 * * * * ? *");
Trigger trigger = TriggerBuilder.newTrigger()
.forJob(jobDetail) // 对应 Job 为 demoJob01
.withIdentity("demoJob02Trigger") // 名字为 demoJob01Trigger
.withSchedule(scheduleBuilder) // 对应 Schedule 为 scheduleBuilder
.build();
// 添加调度任务
scheduler.scheduleJob(jobDetail, trigger);
// scheduler.scheduleJob(jobDetail, Sets.newSet(trigger), true);
}
}
创建 JobDetail 和 Trigger 的代码,其实和 「4.6.1 Bean 自动设置」 是一致的。
在每个单元测试方法的最后,调用 Scheduler#scheduleJob(JobDetail jobDetail, Trigger trigger) 方法,将 JobDetail 和 Trigger 持久化到数据库。
如果想要覆盖数据库中的 Quartz 定时任务的配置,可以调用 Scheduler#scheduleJob(JobDetail jobDetail, Set<? extends Trigger> triggersForJob, boolean replace) 方法,传入 replace = true 进行覆盖配置。
Application
创建 Application.java 类,配置 @SpringBootApplication 注解即可。代码如下:
// Application.java
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
运行 Application 类,启动示例项目。具体的执行日志,和 「3.4 Application」 基本一致,这里就不重复罗列了。
如果胖友想要测试集群下的运行情况,可以再创建 创建 Application02.java 类,配置 @SpringBootApplication 注解即可。代码如下:
// Application02.java
@SpringBootApplication
public class Application02 {
public static void main(String[] args) {
// 设置 Tomcat 随机端口
System.setProperty("server.port", "0");
// 启动 Spring Boot 应用
SpringApplication.run(Application.class, args);
}
}
运行 Application02 类,再次启动一个示例项目。然后,观察输出的日志,可以看到启动的两个示例项目,都会有 DemoJob01 和 DemoJob02 的执行日志。