文章目录
1 摘要
Quartz 作为经典的定时任务框架,有这广泛的应用,支持集群模式。本文将介绍基于 Spring Boot 2.4 集成 Quart 单机模式和集群模式。
Quart 官方文档: http://www.quartz-scheduler.org/documentation
Quartz 数据库表脚本文件: 下载官网压缩包,解压后在 ./quartz-2.4.0-SNAPSHOT/src/org/quartz/impl/jdbcjobstore
目录下找到对应的数据库脚本
2 单机模式
2.1 核心 Maven 依赖
./demo-schedule-quartz/pom.xml
<!-- Quartz 定时任务 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
<version>${springboot.version}</version>
</dependency>
其中 ${springboot.version}
的版本为 2.4.0
2.2 核心代码
2.2.1 定时执行的业务代码
./demo-schedule-quartz/src/main/java/com/ljq/demo/springboot/quartz/service/UserService.java
package com.ljq.demo.springboot.quartz.service;
/**
* @Description: 用户业务层接口
* @Author: junqiang.lu
* @Date: 2020/11/14
*/
public interface UserService {
/**
* 查询所有用户数量
*
* @return
*/
int countAll();
}
./demo-schedule-quartz/src/main/java/com/ljq/demo/springboot/quartz/service/impl/UserServiceImpl.java
package com.ljq.demo.springboot.quartz.service.impl;
import com.ljq.demo.springboot.quartz.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Random;
/**
* @Description: 用户业务层实现类
* @Author: junqiang.lu
* @Date: 2020/11/14
*/
@Slf4j
@Service("userService")
@Transactional(rollbackFor = {Exception.class})
public class UserServiceImpl implements UserService {
/**
* 查询所有用户数量
*
* @return
*/
@Override
public int countAll() {
int count = Math.abs(new Random().nextInt());
log.debug("用户总数为: {}", count);
return count;
}
}
2.2.2 定时任务负载类
./demo-schedule-quartz/src/main/java/com/ljq/demo/springboot/quartz/job/UserJob.java
package com.ljq.demo.springboot.quartz.job;
import com.ljq.demo.springboot.quartz.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.QuartzJobBean;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @Description: 用户模块工作负载
* @Author: junqiang.lu
* @Date: 2020/11/14
*/
@Slf4j
public class UserJob extends QuartzJobBean {
@Autowired
private UserService userService;
private final AtomicInteger counts = new AtomicInteger();
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
log.debug("【定时任务】第【{}】次执行,用户总数:{}", counts.incrementAndGet(), userService.countAll());
}
}
./demo-schedule-quartz/src/main/java/com/ljq/demo/springboot/quartz/job/UserJob2.java
package com.ljq.demo.springboot.quartz.job;
import lombok.extern.slf4j.Slf4j;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;
/**
* @Description: 用户工作负载2
* @Author: junqiang.lu
* @Date: 2020/11/14
*/
@Slf4j
public class UserJob2 extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
log.debug("------定时任务开始执行-------");
}
}
2.2.3 定时任务配置类
./demo-schedule-quartz/src/main/java/com/ljq/demo/springboot/quartz/common/config/QuartzScheduleConfig.java
package com.ljq.demo.springboot.quartz.common.config;
import com.ljq.demo.springboot.quartz.job.UserJob;
import com.ljq.demo.springboot.quartz.job.UserJob2;
import org.quartz.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Description: Quartz 定时任务配置信息
* @Author: junqiang.lu
* @Date: 2020/11/14
*/
@Configuration
public class QuartzScheduleConfig {
public static class UserJobConfig {
/**
* 工作负载名称
*/
private static final String JOB_NAME = "userJob";
/**
* 触发器名称
*/
private static final String TRIGGER_NAME = "userJobTrigger";
@Bean
public JobDetail userJob() {
return JobBuilder.newJob(UserJob.class)
.withIdentity(JOB_NAME)
.storeDurably()
.build();
}
@Bean
public Trigger userJobTrigger(){
SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(5)
.repeatForever();
return TriggerBuilder.newTrigger()
.forJob(JOB_NAME)
.withIdentity(TRIGGER_NAME)
.withSchedule(scheduleBuilder)
.build();
}
}
public static class UserJob2Config {
/**
* 工作负载名称
*/
private static final String JOB_NAME = "userJob2";
/**
* cron 表达式
*/
private static final String CRON_EXP = "0/10 * * * * ? *";
/**
* 触发器名称
*/
private static final String TRIGGER_NAME = "userJob2Trigger";
@Bean
public JobDetail userJob2() {
return JobBuilder.newJob(UserJob2.class)
.withIdentity(JOB_NAME)
.storeDurably()
.build();
}
@Bean
public Trigger userJob2Trigger() {
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(CRON_EXP);
return TriggerBuilder.newTrigger()
.forJob(JOB_NAME)
.withIdentity(TRIGGER_NAME)
.withSchedule(scheduleBuilder)
.build();
}
}
}
2.2.4 SpringBoot 启动类
./demo-schedule-quartz/src/main/java/com/ljq/demo/springboot/quartz/DemoScheduleQuartzApplication.java
package com.ljq.demo.springboot.quartz;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author junqiang.lu
*/
@SpringBootApplication
public class DemoScheduleQuartzApplication {
public static void main(String[] args) {
SpringApplication.run(DemoScheduleQuartzApplication.class, args);
}
}
2.3 application.yml 配置文件
./demo-schedule-quartz/src/main/resources/application.yml
## spring config
spring:
quartz:
scheduler-name: userSchedule
job-store-type: memory
auto-startup: true
startup-delay: 1s
wait-for-jobs-to-complete-on-shutdown: true
overwrite-existing-jobs: false
properties:
org:
quartz:
threadPool:
threadCount: 25
threadPriority: 5
class: org.quartz.simpl.SimpleThreadPool
配置信息参考官方文档: http://www.quartz-scheduler.org/documentation/2.4.0-SNAPSHOT/configuration.html
2.4 运行日志
2020-11-25 19:48:15 | DEBUG | userSchedule_Worker-1 | org.quartz.core.JobRunShell 201| Calling execute on job DEFAULT.userJob
2020-11-25 19:48:15 | DEBUG | userSchedule_Worker-1 | c.l.d.s.quartz.service.impl.UserServiceImpl 28| 用户总数为: 2034175457
2020-11-25 19:48:15 | DEBUG | userSchedule_Worker-1 | com.ljq.demo.springboot.quartz.job.UserJob 27| 【定时任务】第【1】次执行,用户总数:2034175457
2020-11-25 19:48:18 | DEBUG | userSchedule_Worker-2 | org.quartz.core.JobRunShell 201| Calling execute on job DEFAULT.userJob
2020-11-25 19:48:18 | DEBUG | userSchedule_QuartzSchedulerThread | org.quartz.core.QuartzSchedulerThread 291| batch acquisition of 1 triggers
2020-11-25 19:48:18 | DEBUG | userSchedule_Worker-2 | c.l.d.s.quartz.service.impl.UserServiceImpl 28| 用户总数为: 1515972786
2020-11-25 19:48:18 | DEBUG | userSchedule_Worker-2 | com.ljq.demo.springboot.quartz.job.UserJob 27| 【定时任务】第【1】次执行,用户总数:1515972786
2020-11-25 19:48:20 | DEBUG | userSchedule_QuartzSchedulerThread | org.quartz.core.QuartzSchedulerThread 291| batch acquisition of 1 triggers
2020-11-25 19:48:20 | DEBUG | userSchedule_Worker-3 | org.quartz.core.JobRunShell 201| Calling execute on job DEFAULT.userJob2
2020-11-25 19:48:20 | DEBUG | userSchedule_Worker-3 | com.ljq.demo.springboot.quartz.job.UserJob2 17| ------定时任务开始执行-------
2020-11-25 19:48:23 | DEBUG | userSchedule_QuartzSchedulerThread | org.quartz.core.QuartzSchedulerThread 291| batch acquisition of 1 triggers
2020-11-25 19:48:23 | DEBUG | userSchedule_Worker-4 | org.quartz.core.JobRunShell 201| Calling execute on job DEFAULT.userJob
2020-11-25 19:48:23 | DEBUG | userSchedule_Worker-4 | c.l.d.s.quartz.service.impl.UserServiceImpl 28| 用户总数为: 807472199
2020-11-25 19:48:23 | DEBUG | userSchedule_Worker-4 | com.ljq.demo.springboot.quartz.job.UserJob 27| 【定时任务】第【1】次执行,用户总数:807472199
3 集群模式
3.1 核心 Maven 依赖
./demo-schedule-quartz-group/pom.xml
<!-- Quartz 定时任务 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
<version>${springboot.version}</version>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- jdbc -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<version>${springboot.version}</version>
</dependency>
其中 ${springboot.version}
的版本为 2.4.0
3.2 核心代码
3.2.1 定时任务业务代码
与单机模式一致
./demo-schedule-quartz-group/src/main/java/com/ljq/demo/springboot/quartz/group/service/UserService.java
./demo-schedule-quartz-group/src/main/java/com/ljq/demo/springboot/quartz/group/service/impl/UserServiceImpl.java
3.2.2 定时任务负载类
基本与单机模式一致,但是在类上添加了 @DisallowConcurrentExecution
注解,意为禁止并发运行,从而保证了在集群环境中,定时任务一次只有一台服务器在运行
./demo-schedule-quartz-group/src/main/java/com/ljq/demo/springboot/quartz/group/job/UserJob.java
package com.ljq.demo.springboot.quartz.group.job;
import com.ljq.demo.springboot.quartz.group.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.QuartzJobBean;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @Description: 用户模块工作负载
* @Author: junqiang.lu
* @Date: 2020/11/14
*/
@Slf4j
@DisallowConcurrentExecution
public class UserJob extends QuartzJobBean {
@Autowired
private UserService userService;
private final AtomicInteger counts = new AtomicInteger();
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
log.debug("【定时任务】第【{}】次执行,用户总数:{}", counts.incrementAndGet(), userService.countAll());
}
}
./demo-schedule-quartz-group/src/main/java/com/ljq/demo/springboot/quartz/group/job/UserJob2.java
package com.ljq.demo.springboot.quartz.group.job;
import lombok.extern.slf4j.Slf4j;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;
/**
* @Description: 用户工作负载2
* @Author: junqiang.lu
* @Date: 2020/11/14
*/
@Slf4j
@DisallowConcurrentExecution
public class UserJob2 extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
log.debug("------定时任务开始执行-------");
}
}
3.2.3 定时任务配置类
与单机模式一致
./demo-schedule-quartz-group/src/main/java/com/ljq/demo/springboot/quartz/group/common/config/QuartzScheduleConfig.java
3.2.4 application.yml 配置文件
这里配置两个数据源,是为了将 Quartz 数据源和业务的数据源区分开, Quartz 使用独立的数据源效率更高
./demo-schedule-quartz-group/src/main/resources/application.yml
## config
## server
server:
port: 8551
## spring config
spring:
datasource:
user:
url: "jdbc:mysql://172.16.140.10:3306/demo?useUnicode=true&characterEncoding=utf8\
&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2b8\
&useSSL=true&allowMultiQueries=true&autoReconnect=true&nullCatalogMeansCurrent=true\
&nullCatalogMeansCurrent=true"
username: root
password: "Qwert12345!"
driver-class-name: com.mysql.cj.jdbc.Driver
quartz:
url: "jdbc:mysql://172.16.140.10:3306/schedule_quartz?useUnicode=true&characterEncoding=utf8\
&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2b8\
&useSSL=true&allowMultiQueries=true&autoReconnect=true&nullCatalogMeansCurrent=true\
&nullCatalogMeansCurrent=true"
username: root
password: "Qwert12345!"
driver-class-name: com.mysql.cj.jdbc.Driver
quartz:
scheduler-name: userSchedule
job-store-type: jdbc
auto-startup: true
startup-delay: 1s
wait-for-jobs-to-complete-on-shutdown: true
overwrite-existing-jobs: false
jdbc:
initialize-schema: never
properties:
org:
quartz:
jobStore:
dataSource: quartzDataSource
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.2.5 数据源配置类
./demo-schedule-quartz-group/src/main/java/com/ljq/demo/springboot/quartz/group/common/config/DataSourceConfig.java
package com.ljq.demo.springboot.quartz.group.common.config;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.autoconfigure.quartz.QuartzDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.util.StringUtils;
import javax.sql.DataSource;
/**
* @Description: 数据源配置
* @Author: junqiang.lu
* @Date: 2020/11/17
*/
@Configuration
public class DataSourceConfig {
/**
* 用户数据源配置(主数据源)
*
* @return
*/
@Primary
@Bean("userDatasourceProperties")
@ConfigurationProperties(prefix = "spring.datasource.user")
public DataSourceProperties userDataSourceProperties() {
return new DataSourceProperties();
}
/**
* 用户数据源(主数据源)
*
* @return
*/
@Primary
@Bean("userDataSource")
@ConfigurationProperties(prefix = "spring.datasource.user.hikari")
public DataSource userDataSource() {
DataSourceProperties properties = this.userDataSourceProperties();
return createHikariDataSource(properties);
}
/**
* Quartz 数据源配置
*
* @return
*/
@Bean("quartzDataSourceProperties")
@ConfigurationProperties(prefix = "spring.datasource.quartz")
public DataSourceProperties quartzDataSourceProperties() {
return new DataSourceProperties();
}
/**
* Quartz 数据源
*
* @return
*/
@Bean("quartzDataSource")
@ConfigurationProperties(prefix = "spring.datasource.quartz.hikari")
@QuartzDataSource
public DataSource quartzDataSource() {
DataSourceProperties properties = this.quartzDataSourceProperties();
return createHikariDataSource(properties);
}
/**
* 创建 Hikari 数据库连接池
*
* @param properties
* @return
*/
private HikariDataSource createHikariDataSource(DataSourceProperties properties) {
HikariDataSource dataSource = properties.initializeDataSourceBuilder()
.type(HikariDataSource.class)
.build();
if (StringUtils.hasText(properties.getName())) {
dataSource.setPoolName(properties.getName());
}
return dataSource;
}
}
@Primary
表名数据源为主数据源
@QuartzDataSource
指明该数据源为 Quartz 框架的数据源
3.2.6 SpringBoot 启动类
这里为了模拟集群工作环境,写两个 SpringBoot 启动类,端口号区分开
./demo-schedule-quartz-group/src/main/java/com/ljq/demo/springboot/quartz/group/DemoScheduleQuartzGroupApplication.java
package com.ljq.demo.springboot.quartz.group;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author apple
*/
@SpringBootApplication
public class DemoScheduleQuartzGroupApplication {
public static void main(String[] args) {
SpringApplication.run(DemoScheduleQuartzGroupApplication.class, args);
}
}
./demo-schedule-quartz-group/src/main/java/com/ljq/demo/springboot/quartz/group/DemoScheduleQuartzGroupApplication2.java
package com.ljq.demo.springboot.quartz.group;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @Description: Quartz 定时任务集群模式应用启动类2
* @Author: junqiang.lu
* @Date: 2020/11/18
*/
@SpringBootApplication
public class DemoScheduleQuartzGroupApplication2 {
public static void main(String[] args) {
System.setProperty("server.port", "8552");
SpringApplication.run(DemoScheduleQuartzGroupApplication2.class);
}
}
依次将启动这两个程序,即可实现集群工作的效果,观察两个控制台的输出日志,会发现两边都会有日志输出,但是在同一时间只有一个定时任务在运行
3.2.7 手动创建定时任务
直接启动 SpringBoot 项目,程序会自动创建定时任务,但定时任务也可以通过手动的方式创建,可以选择是否覆盖已有任务
测试类
./demo-schedule-quartz-group/src/test/java/com/ljq/demo/springboot/quartz/group/common/config/QuartzScheduleConfigTest.java
package com.ljq.demo.springboot.quartz.group.common.config;
import com.ljq.demo.springboot.quartz.group.DemoScheduleQuartzGroupApplication;
import com.ljq.demo.springboot.quartz.group.job.UserJob;
import com.ljq.demo.springboot.quartz.group.job.UserJob2;
import org.junit.jupiter.api.Test;
import org.mockito.internal.util.collections.Sets;
import org.quartz.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
/**
* 定时任务配置测试类
*/
@SpringBootTest(classes = DemoScheduleQuartzGroupApplication.class)
class QuartzScheduleConfigTest {
@Autowired
private Scheduler scheduler;
private static final String USER_JOB_DETAIL_NAME = "userJob001";
private static final String USER_JOB_TRIGGER_NAME = "userJobTrigger001";
private static final String USER_JOB_2_DETAIL_NAME = "userJob002";
private static final String USER_JOB_2_TRIGGER_NAME = "userJob2Trigger002";
private static final String USER_JOB_2_CRON = "0/10 * * * * ? *";
/**
* 手动添加用户定时任务配置
*
* @throws SchedulerException
*/
@Test
public void addUserJobConfig() throws SchedulerException {
// 创建 JobDetail
JobDetail jobDetail = JobBuilder.newJob(UserJob.class)
.withIdentity(USER_JOB_DETAIL_NAME)
.storeDurably()
.build();
// 创建 Trigger
SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(5)
.repeatForever();
Trigger trigger = TriggerBuilder.newTrigger()
.forJob(jobDetail)
.withIdentity(USER_JOB_TRIGGER_NAME)
.withSchedule(scheduleBuilder)
.build();
// 添加调度任务
// 不覆盖已有任务
// scheduler.scheduleJob(jobDetail, trigger);
// 覆盖已有任务
scheduler.scheduleJob(jobDetail, Sets.newSet(trigger), true);
}
/**
* 手动创建用户2定时任务
*
* @throws SchedulerException
*/
@Test
public void addUserJob2Config() throws SchedulerException {
// 创建 JobDetail
JobDetail jobDetail = JobBuilder.newJob(UserJob2.class)
.withIdentity(USER_JOB_2_DETAIL_NAME)
.storeDurably()
.build();
// 创建 Trigger
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(USER_JOB_2_CRON);
Trigger trigger = TriggerBuilder.newTrigger()
.forJob(jobDetail)
.withIdentity(USER_JOB_2_TRIGGER_NAME)
.withSchedule(scheduleBuilder)
.build();
// 添加调度任务
// 不覆盖已有任务
// scheduler.scheduleJob(jobDetail, trigger);
// 覆盖已有任务
scheduler.scheduleJob(jobDetail, Sets.newSet(trigger), true);
}
}
无论是手动创建还是自动创建,效果是一样的
4 推荐参考资料
Quartz 官方: http://www.quartz-scheduler.org/documentation/2.4.0-SNAPSHOT/index.html
5 Github 源码
Gtihub 源码地址 : https://github.com/Flying9001/springBootDemo
个人公众号:404Code,分享半个互联网人的技术与思考,感兴趣的可以关注.