最近换项目组,发现项目中定时任务使用的是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*/@Beanpublic 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 {@Overridepublic void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {//todoSimpleDateFormat 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=truedriver: com.mysql.cj.jdbc.DrivermaxConnections: 5user: rootpassword: 123456connectionProvider:class: com.example.demo.config.QuartzConnectionProviderscheduler:instanceName: ClusterQuartzinstanceId: AUTOrmi:export: falseproxy: falsewrapJobExecutionInUserTransaction: falsejobStore:driverDelegateClass: org.quartz.impl.jdbcjobstore.PostgreSQLDelegatetablePrefix: QRTZ_isClustered: trueacquireTriggersWithinLock: truemisfireThreshold: 5000useProperties: truedataSource: myDSthreadPool:class: org.quartz.simpl.SimpleThreadPoolthreadCount: 1threadPriority: 5threadsInheritContextClassLoaderOfInitializingThread: 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配置*/@Beanpublic 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 启动时更新己存在的JobschedulerFactoryBean.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);elsedatasource.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 {@Overridepublic void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {//todoSimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm");String format = simpleDateFormat.format(new Date());log.info("{}开始执行定时任务2: {}",format ,jobExecutionContext.getJobDetail().getKey().getName());}}
启动项目后,可以看出数据库中定时任务的表达式已经存储在表中
如图所示,定时任务的信息存储在表中。