最近换项目组,发现项目中定时任务使用的是quartz框架,上一篇文章[springboot定时任务]也是使用的quartz,只不过实现方式不同,于是整理下
定时任务常用方法有Quartz,Spring自带的Schedule框架
Quartz基础知识
quartz常用组件:调度器,触发器,作业。
常用类:jobdetail(定时任务。描述job的核心逻辑);trigger(触发器。一个触发器对应一个定时任务,而一个定时任务可以对应多个触发器);schedule(调度器。管理触发器及定时任务的协调工作,一个schedule可以有多个trigger和jobdetail)
quartz提供两种作业存储类型,RAMJobStore和JDBC作业。
RAMJobStore是内存存储。数据访问快,但由于是内存存储。若服务挂掉,运行的定时任务信息就会丢失。
JDBC是数据库存储。通过quartz.properties文件,持久化任务调度信息。若应用挂掉,重启后会继续执行定时任务。
集群模式
Quartz框架
若部署多台服务器,定时任务在某一时刻只能有一个节点执行,则使用持久化任务调度信息方案
quartz默认的集群方案,保证同一时刻相同触发器只能一个节点执行;若节点执行失败,则会分派到另一节点,中途也会自动检查失效的定时调度,若不成功,则其他节点会继续执行。定时任务在集群环境下默认是多台服务器会同时执行,这时需要结合项目自己程序判断。
quartz定时任务的配置文件中有一属性 org.quartz.jobStore.isClustered=true则为集群方式,自动判断节点状态。
@Schedule注解定时任务
@Schedule注解会定时跑服务。但是若部署多台服务器时,则会导致每台服务器都会执行一次,解决办法1.指定某台服务器执行 2.使用redis锁 3.数据库状态标识记录。但这种方式若是服务器时间一样,并发时数据库标识在查询时未来得及更新,也会导致多台服务器执行定时任务。详情可见[定时任务的时间修改后立即生效]
分布式多节点多活应用同一定时任务,多节点同时执行配置
pom.xml
<!--定时任务-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
<version>2.7.18</version>
</dependency>
QuartzConfig类
package com.example.demo.config;
import com.example.demo.jobs.Test1Job;
import org.quartz.CronTrigger;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.CronTriggerFactoryBean;
import org.springframework.scheduling.quartz.JobDetailFactoryBean;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import java.util.Objects;
/**
* @Auther: lr
* @Date: 2024/3/28 14:34
* @Description:
*/
@Configuration
public class QuartzConfig {
@Value("${cron.test1}")
private String test1Cron;
/**
* 定时任务的信息载体
* 通过FactoryBean来间接创建Job对象,若有多个Job对象,需定义多次方法
* 需要重写Factory相关代码,实现spring容器管理jobdetail
* @return
*/
@Bean(name ="test1JobDetailBean" )
public JobDetailFactoryBean initTest1JobDetailBean(){
JobDetailFactoryBean jobDetailFactoryBean=new JobDetailFactoryBean();
jobDetailFactoryBean.setJobClass(Test1Job.class);
return jobDetailFactoryBean;
}
/**
* 触发器
* 就是Trigger的一个实现类型,其中用于定义周期时间的CronScheduleBuilder
* 实际上,CronTrigger是用于管理一个cron表达式的类型
* @return
*/
@Bean(name = "test1CronTriggerBean")
public CronTriggerFactoryBean initTest1CronTriggerBean(){
CronTriggerFactoryBean cronTriggerFactoryBean=new CronTriggerFactoryBean();
JobDetailFactoryBean jobDetailFactoryBean = this.initTest1JobDetailBean();
cronTriggerFactoryBean.setJobDetail(Objects.requireNonNull(jobDetailFactoryBean.getObject()));
cronTriggerFactoryBean.setCronExpression(test1Cron);
return cronTriggerFactoryBean;
}
/**
* 任务调度器
* @param customJobFactory 定时任务的信息载体
* @param cronTriggerFactoryBeans 触发器
* @return
*/
@Bean
public SchedulerFactoryBean initSchedulerFactoryBean(CustomJobFactory customJobFactory,
CronTriggerFactoryBean[] cronTriggerFactoryBeans){
SchedulerFactoryBean factoryBean = new SchedulerFactoryBean();
factoryBean.setJobFactory(customJobFactory);
CronTrigger[] cronTriggers=new CronTrigger[cronTriggerFactoryBeans.length];
for(int i=0;i<cronTriggerFactoryBeans.length;i++){
cronTriggers[i]=cronTriggerFactoryBeans[i].getObject();
}
//注册触发器,一个Scheduler可以注册若干个触发器
factoryBean.setTriggers(cronTriggers);
//为Scheduler设置Jobdetail的工厂,可以覆盖掉springboot默认提供的工厂,保证jobdetail自动装配有效
factoryBean.setJobFactory(customJobFactory);
return factoryBean;
}
}
Test1Job
package com.example.demo.jobs;
import lombok.extern.slf4j.Slf4j;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @Auther: lr
* @Date: 2024/3/28 14:42
* @Description:
*/
@Slf4j
public class Test1Job implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
//todo
SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-mm-dd HH:mm");
String format = simpleDateFormat.format(new Date());
log.info("{}开始执行定时任务1: {}",format ,jobExecutionContext.getJobDetail().getKey().getName());
}
}
运行结果
可以看到多台机器都会执行到该定时任务
分布式多节点多活应用同一定时任务,单节点执行配置
pom.xml
<!--定时任务-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
<version>2.7.18</version>
</dependency>
<!--引入druid数据源-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.6</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!--定时任务-->
quartz.yml
org:
quartz:
dataSource:
myDS:
URL: jdbc:mysql://127.0.0.1:3306/test?serverTimezone=Hongkong&useUnicode=true&characterEncoding=UTF-8&useSSL=false&autoReconnect=true
driver: com.mysql.cj.jdbc.Driver
maxConnections: 5
user: root
password: 123456
connectionProvider:
class: com.example.demo.config.QuartzConnectionProvider
scheduler:
instanceName: ClusterQuartz
instanceId: AUTO
rmi:
export: false
proxy: false
wrapJobExecutionInUserTransaction: false
jobStore:
driverDelegateClass: org.quartz.impl.jdbcjobstore.PostgreSQLDelegate
tablePrefix: QRTZ_
isClustered: true
acquireTriggersWithinLock: true
misfireThreshold: 5000
useProperties: true
dataSource: myDS
threadPool:
class: org.quartz.simpl.SimpleThreadPool
threadCount: 1
threadPriority: 5
threadsInheritContextClassLoaderOfInitializingThread: true
数据库表整理
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;
QuartzSingleConfig配置文件
package com.example.demo.config;
import com.example.demo.jobs.Test2Job;
import org.quartz.CronTrigger;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.PropertiesFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.scheduling.quartz.CronTriggerFactoryBean;
import org.springframework.scheduling.quartz.JobDetailFactoryBean;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import javax.sql.DataSource;
import java.io.IOException;
import java.util.Objects;
import java.util.Properties;
/**
* @Auther: lr
* @Date: 2024/3/28 14:34
* @Description:
*/
@Configuration
public class QuartzSingleConfig {
@Value("${cron.test2}")
private String test2Cron;
/**
* 加载quartz配置
*/
@Bean
public Properties quartzProperties() throws IOException {
PropertiesFactoryBean propertiesFactoryBean=new PropertiesFactoryBean();
propertiesFactoryBean.setLocation(new ClassPathResource("/quartz.yml"));
propertiesFactoryBean.afterPropertiesSet();
return propertiesFactoryBean.getObject();
}
/**
* 定时任务的信息载体
*
* @return
*/
@Bean(name = "test2JobDetailBean")
public JobDetailFactoryBean initTest2JobDetailBean() {
JobDetailFactoryBean jobDetailFactoryBean = new JobDetailFactoryBean();
jobDetailFactoryBean.setJobClass(Test2Job.class);
jobDetailFactoryBean.setDurability(true);
return jobDetailFactoryBean;
}
/**
* 触发器
*
* @return
*/
@Bean(name = "test2CronTriggerBean")
public CronTriggerFactoryBean initTest2CronTriggerBean() {
CronTriggerFactoryBean cronTriggerFactoryBean = new CronTriggerFactoryBean();
JobDetailFactoryBean jobDetailFactoryBean = this.initTest2JobDetailBean();
cronTriggerFactoryBean.setJobDetail(Objects.requireNonNull(jobDetailFactoryBean.getObject()));
cronTriggerFactoryBean.setCronExpression(test2Cron);
return cronTriggerFactoryBean;
}
/**
* 任务调度器
*
* @param customJobFactory 定时任务的信息载体
* @param cronTriggerFactoryBeans 触发器
* @return
*/
@Bean(name = "schedulerFactoryBean")
public SchedulerFactoryBean initSchedulerFactoryBean(CustomJobFactory customJobFactory,
CronTriggerFactoryBean[] cronTriggerFactoryBeans,
@Qualifier("dataSource") DataSource dataSource) throws IOException {
SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
//自动覆盖quartz.properties配置的数据源
schedulerFactoryBean.setDataSource(dataSource);
//设置quartz的配置文件
schedulerFactoryBean.setQuartzProperties(quartzProperties());
schedulerFactoryBean.setJobFactory(customJobFactory);
CronTrigger[] cronTriggers = new CronTrigger[cronTriggerFactoryBeans.length];
for (int i = 0; i < cronTriggerFactoryBeans.length; i++) {
cronTriggers[i] = cronTriggerFactoryBeans[i].getObject();
}
schedulerFactoryBean.setTriggers(cronTriggers);
// 用于quartz集群,QuartzScheduler 启动时更新己存在的Job
schedulerFactoryBean.setOverwriteExistingJobs(true);
//延长启动
schedulerFactoryBean.setStartupDelay(1);
return schedulerFactoryBean;
}
}
Durid连接池quartz工具类 QuartzConnectionProvider
package com.example.demo.config;
import com.alibaba.druid.pool.DruidDataSource;
import lombok.Data;
import org.quartz.SchedulerException;
import org.quartz.utils.ConnectionProvider;
import java.sql.Connection;
import java.sql.SQLException;
/**
* @Auther: lr
* @Date: 2024/3/28 17:01
* @Description:
*/
@Data
public class QuartzConnectionProvider implements ConnectionProvider {
/*
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*
* 常量配置,与quartz.properties文件的key保持一致(去掉前缀),同时提供set方法,Quartz框架自动注入值。
*
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/
//JDBC驱动
public String driver;
//JDBC连接串
public String URL;
//数据库用户名
public String user;
//数据库用户密码
public String password;
//数据库最大连接数
public int maxConnection;
//数据库SQL查询每次连接返回执行到连接池,以确保它仍然是有效的。
public String validationQuery;
private boolean validateOnCheckout;
private int idleConnectionValidationSeconds;
public String maxCachedStatementsPerConnection;
private String discardIdleConnectionsSeconds;
public static final int DEFAULT_DB_MAX_CONNECTIONS = 10;
public static final int DEFAULT_DB_MAX_CACHED_STATEMENTS_PER_CONNECTION = 120;
//Druid连接池
private DruidDataSource datasource;
/*
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*
* 接口实现
*
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/
public Connection getConnection() throws SQLException {
return datasource.getConnection();
}
public void shutdown() throws SQLException {
datasource.close();
}
public void initialize() throws SQLException {
if (this.URL == null) {
throw new SQLException("DBPool could not be created: DB URL cannot be null");
}
if (this.driver == null) {
throw new SQLException("DBPool driver could not be created: DB driver class name cannot be null!");
}
if (this.maxConnection < 0) {
throw new SQLException("DBPool maxConnectins could not be created: Max connections must be greater than zero!");
}
datasource = new DruidDataSource();
try {
datasource.setDriverClassName(this.driver);
} catch (Exception e) {
try {
throw new SchedulerException("Problem setting driver class name on datasource: " + e.getMessage(), e);
} catch (SchedulerException e1) {
}
}
datasource.setUrl(this.URL);
datasource.setUsername(this.user);
datasource.setPassword(this.password);
datasource.setMaxActive(this.maxConnection);
datasource.setMinIdle(1);
datasource.setMaxWait(0);
datasource.setMaxPoolPreparedStatementPerConnectionSize(this.DEFAULT_DB_MAX_CACHED_STATEMENTS_PER_CONNECTION);
if (this.validationQuery != null) {
datasource.setValidationQuery(this.validationQuery);
if (!this.validateOnCheckout)
datasource.setTestOnReturn(true);
else
datasource.setTestOnBorrow(true);
datasource.setValidationQueryTimeout(this.idleConnectionValidationSeconds);
}
}
}
Test2Job类
package com.example.demo.jobs;
import lombok.extern.slf4j.Slf4j;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @Auther: lr
* @Date: 2024/3/29 9:29
* @Description:
*/
@Slf4j
public class Test2Job implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
//todo
SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm");
String format = simpleDateFormat.format(new Date());
log.info("{}开始执行定时任务2: {}",format ,jobExecutionContext.getJobDetail().getKey().getName());
}
}
启动项目后,可以看出数据库中定时任务的表达式已经存储在表中
如图所示,定时任务的信息存储在表中。