如果对quartz集群原理和配置不太了解,大家可参考文章:Quartz集群原理及配置应用,这里我就不细说了
SpringBoot实现定时任务两种方式:
1、基于 spring的@Scheduled注解;
可参考文章:SpringBoot整合Quartz定时任务(基于注解方式),这种方式比较便捷,在单台服务部署情况下可使用,若部署多台机器,到了时间点,便会同时均开始这个执行定时任务(问题)。
2、使用Quartz(集群环境)实现:
2.1 Quzrtz集群的实现方式在于11张表,集群节点相互之间不通信,而是通过相同的数据库表来感知到另一节点的,因此Quartz集群依赖于数据库,必须先创建数据库表,数据表是官方提供的 ,有11张表,如下:
项目结构:
2.2 创建maven项目pom.xml添加依赖:
4.0.0
com.test.quartz
quartz
1.0-SNAPSHOT
junit
junit
4.11
test
mysql
mysql-connector-java
8.0.13
org.mybatis.generator
mybatis-generator-core
1.3.7
org.springframework.boot
spring-boot-devtools
true
2.1.0.RELEASE
org.springframework.boot
spring-boot-starter-actuator
2.1.0.RELEASE
log4j-api
org.apache.logging.log4j
org.springframework.boot
spring-boot-starter-websocket
2.1.0.RELEASE
log4j-api
org.apache.logging.log4j
spring-boot-starter-logging
org.springframework.boot
org.springframework.boot
spring-boot-starter-jdbc
2.1.0.RELEASE
org.springframework.boot
spring-boot-starter-test
test
2.1.0.RELEASE
org.apache.commons
commons-lang3
3.4
org.apache.httpcomponents
httpclient
4.5.2
org.apache.curator
curator-framework
2.12.0
netty
io.netty
log4j
log4j
guava
com.google.guava
org.springframework.boot
spring-boot-starter-quartz
2.1.0.RELEASE
org.apache.logging.log4j
log4j-api
2.8.2
org.apache.logging.log4j
log4j-core
2.8.2
${artifactId}-${version}
org.jacoco
jacoco-maven-plugin
0.8.2
prepare-agent
prepare-agent
report
prepare-package
report
post-unit-test
test
report
target/jacoco.exec
target/jacoco.exec
com.github.wvengen
proguard-maven-plugin
2.0.11
package
proguard
6.0.3
${project.groupId}
quartz
${project.build.finalName}.jar
${project.build.finalName}.jar
true
${java.home}/lib/rt.jar
net.sf.proguard
proguard-base
6.0.3
org.springframework.boot
spring-boot-maven-plugin
2.0.4.RELEASE
repackage
com.test.quartz.application.CenterApplication
org.apache.maven.plugins
maven-compiler-plugin
6
2.3 创建springboot启动类(CenterApplication):
package com.test.quartz.application;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.stereotype.Component;
/**
* @author [email protected]
* @date 2019/2/13
* @description: springboot启动类
*/
@SpringBootApplication
@ComponentScan(basePackages = "com.test.quartz.*")
@EnableScheduling
public class CenterApplication {
private static final Logger LOG = LoggerFactory.getLogger(CenterApplication.class);
private static final String START_PACKAGE = "com.test.quartz.";
public static class CustomGenerator implements BeanNameGenerator {
@Override
public String generateBeanName(BeanDefinition beanDefinition, BeanDefinitionRegistry beanDefinitionRegistry) {
String beanName = beanDefinition.getBeanClassName();
if (beanName.startsWith(START_PACKAGE)) {
return beanName;
}
try {
Class> aClass = Class.forName(beanName);
Component annotation = aClass.getAnnotation(Component.class);
if (annotation == null) {
return beanName;
}
String value = annotation.value();
if (StringUtils.isNotBlank(value)) {
return value;
}
} catch (Exception e) {
LOG.error("generateBeanName method throw a exception", e);
}
return beanName;
}
}
public static void main(String... args) {
ApplicationContext ctx = new SpringApplicationBuilder(CenterApplication.class)
.beanNameGenerator(new CustomGenerator())
.run(args);
String[] activeProfiles = ctx.getEnvironment().getActiveProfiles();
for (String profile : activeProfiles) {
LOG.info("Spring Boot 使用profile为: application-{}.yml", profile);
}
}
}
2.4 配置文件:
application.yml
spring:
profiles:
active: win
#active: linux
application-win.yml(window上的配置,一般喜欢将window和linux分开来写,后期部署时方便切换,liunx用不上暂时就不放了):
server:
port: 7008
logging:
config: classpath:properties/logback-spring.xml
#config: classpath:properties/logback-spring.xml
charset:
encoding: utf-8
spring:
quartz:
job-store-type: jdbc
日志文件logback-spring.xml ,通过在配置文件中指定生效的环境和日志的级别 :
QuartzConfiguration配置文件:org.quartz.jobStore.isClustered 是否启用集群,设置为true
package com.test.quartz.configuration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.quartz.SchedulerFactoryBeanCustomizer;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import javax.sql.DataSource;
import java.util.Properties;
/**
* @author [email protected]
* @date 2019/2/13
* @description: quartz配置
*/
@Configuration
public class QuartzConfiguration implements SchedulerFactoryBeanCustomizer {
@Autowired
DataSource dataSource;
@Override
public void customize(SchedulerFactoryBean schedulerFactoryBean) {
schedulerFactoryBean.setQuartzProperties(quartzProperties());
schedulerFactoryBean.setStartupDelay(2);
schedulerFactoryBean.setAutoStartup(true);
schedulerFactoryBean.setDataSource(dataSource);
//如果这个覆盖配置为false,quratz启动以后将以数据库的数据为准,配置文件的修改不起作用。
schedulerFactoryBean.setOverwriteExistingJobs(true);
}
/**
* 设置参数
*
* @return quartz参数
*/
private Properties quartzProperties() {
Properties prop = new Properties();
// 调度标识名 集群中每一个实例都必须使用相同的名称
prop.put("org.quartz.scheduler.instanceName", "quartz-01");
// ID设置为自动获取 每一个必须不同
prop.put("org.quartz.scheduler.instanceId", "AUTO");
// 数据保存方式为持久化
prop.put("org.quartz.jobStore.class", "org.quartz.impl.jdbcjobstore.JobStoreTX");
// 数据库方言
prop.put("org.quartz.jobStore.driverDelegateClass", "org.quartz.impl.jdbcjobstore.StdJDBCDelegate");
// 表前缀
prop.put("org.quartz.jobStore.tablePrefix", "t_jk_qrtz_");
// 是否启用集群功能
prop.put("org.quartz.jobStore.isClustered", "true");
// 设置一个频度(毫秒),用于实例报告给集群中的其他实例。这会影响到侦测失败实例的敏捷度。它只用于设置了 isClustered 为 true 的时候。
prop.put("org.quartz.jobStore.clusterCheckinInterval", "20000");
// 这是 JobStore 能处理的错过触发的 Trigger 的最大数量。处理太多(超过两打) 很快会导致数据库表被锁定够长的时间,这样就妨碍了触发别的(还未错过触发) trigger 执行的性能。
prop.put("org.quartz.jobStore.maxMisfiresToHandleAtATime", "1");
// 当前时间与下一次执行的时间差大于改值时认为missFire(错过触发),根据missFire原则处理任务;若小于该值直接执行任务。默认60000(60秒)
prop.put("org.quartz.jobStore.misfireThreshold", "120000");
// 值为 True 时告诉 Quartz (当使用 JobStoreTX 或 CMT 时),调用 JDBC 连接的 setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE) 方法,设置事务隔离级别为串行
// 这能助于防止某些数据库在高负荷和长事物时的锁超时。
prop.put("org.quartz.jobStore.txIsolationLevelSerializable", "true");
// 执行任务的线程池配置
prop.put("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool");
prop.put("org.quartz.threadPool.threadCount", "10");
prop.put("org.quartz.threadPool.threadPriority", "5");
// 指定Quartz生成的线程是否继承初始化线程的上下文类加载器
prop.put("org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread", "true");
// 跳过更新检查
prop.put("org.quartz.scheduler.skipUpdateCheck", true);
//quartz 插件配置
prop.put("org.quartz.plugin.triggHistory.class", "org.quartz.plugins.history.LoggingJobHistoryPlugin");
prop.put("org.quartz.plugin.shutdownhook.class", "org.quartz.plugins.management.ShutdownHookPlugin");
prop.put("org.quartz.plugin.shutdownhook.cleanShutdown", "true");
return prop;
}
}
mysql配置文件:MysqlConfig
package com.test.quartz.configuration;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
/**
* @author [email protected]
* @date 2019/2/13
* @description: mysql连接配置
*/
@Component
public class MysqlConfig {
@Bean
public DataSource dataSource() {
HikariDataSource hikariDataSource = new HikariDataSource();
hikariDataSource.setJdbcUrl("jdbc:mysql://localhost:3306/hiseejk?characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai");
hikariDataSource.setUsername("root");
hikariDataSource.setPassword("root");
hikariDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
hikariDataSource.setPoolName("hiseejk");
// 连接超时时间
hikariDataSource.setConnectionTimeout(30000);
// 最大池大小
hikariDataSource.setMaximumPoolSize(20);
// 默认连接数
hikariDataSource.setMinimumIdle(10);
hikariDataSource.setIdleTimeout(600000);
hikariDataSource.setMaxLifetime(1800000);
return hikariDataSource;
}
}
任务实体类:
package com.test.quartz.quartz;
import java.util.Map;
/**
* @author [email protected]
* @date 2019/2/13
* @description: 任务实体类
*/
public class JobView {
private String name;
private String group;
private String cronExpression;
private String nextFireTime;
private String status;
Map params;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getGroup() {
return group;
}
public void setGroup(String group) {
this.group = group;
}
public String getCronExpression() {
return cronExpression;
}
public void setCronExpression(String cronExpression) {
this.cronExpression = cronExpression;
}
public String getNextFireTime() {
return nextFireTime;
}
public void setNextFireTime(String nextFireTime) {
this.nextFireTime = nextFireTime;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public Map getParams() {
return params;
}
public void setParams(Map params) {
this.params = params;
}
}
任务工具类QuartzJobUtils:
package com.test.quartz.quartz;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.curator.shaded.com.google.common.collect.Lists;
import org.apache.curator.shaded.com.google.common.collect.Maps;
import org.apache.http.client.utils.DateUtils;
import org.quartz.*;
import org.quartz.impl.matchers.GroupMatcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.quartz.QuartzJobBean;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* @author [email protected]
* @date 2019/2/13
* @description:
*/
public class QuartzJobUtils {
private static final Logger LOG = LoggerFactory.getLogger(QuartzJobUtils.class);
private QuartzJobUtils() {
super();
}
/**
* 创建quartz定时任务
*
* @param scheduler 调度器
* @param jobClass 任务class,必须继承QuartzJobBean
* @param cronExpression cron表达式
* @param jobDataMap 任务信息
*/
public static void createJob(Scheduler scheduler, String name, Class extends QuartzJobBean> jobClass, String cronExpression, JobDataMap jobDataMap) {
//任务所属分组
String group = "TEST";
LOG.info("----createJob start ,name:{},group:{}", name, group);
if (exist(scheduler, name, group)) {
LOG.info("----createJob fail ,job already existed,name:{},group:{}", name, group);
return;
}
//cron表达式封装
//missfire处理 withMisfireHandlingInstructionDoNothing 错过触发时间时,不执行执行的,等待下一个执行时间
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression).withMisfireHandlingInstructionDoNothing();
//创建任务
JobDetail jobDetail;
if (jobDataMap != null) {
//requestRecovery(true)指在集群中,一个scheduler执行job失败,将会被另外一个scheduler执行
jobDetail = JobBuilder.newJob(jobClass).withIdentity(name, group).usingJobData(jobDataMap).requestRecovery(true).build();
} else {
jobDetail = JobBuilder.newJob(jobClass).withIdentity(name, group).requestRecovery(true).build();
}
//创建任务触发器
Trigger trigger = TriggerBuilder.newTrigger().withIdentity(name, group).withSchedule(scheduleBuilder).build();
//将触发器与任务绑定到调度器内
try {
Date firstFireTime = scheduler.scheduleJob(jobDetail, trigger);
LOG.info("----createJob success,firstFireTime:{}", DateFormatUtils.format(firstFireTime, "yyyy-MM-dd HH:mm:ss"));
} catch (SchedulerException e) {
LOG.error("-----createJob exception", e);
}
}
/**
* 创建quartz定时任务
*
* @param scheduler 调度器
* @param jobClass 任务class,必须继承QuartzJobBean
* @param cronExpression cron表达式
*/
public static void createJob(Scheduler scheduler, String name, Class extends QuartzJobBean> jobClass, String cronExpression) {
createJob(scheduler, name, jobClass, cronExpression, null);
}
public static void close(Scheduler scheduler, JobExecutionContext context) {
TriggerKey triggerKey = context.getTrigger().getKey();
JobKey jobKey = context.getJobDetail().getKey();
LOG.info("----quartzJob close,jobName:{}", jobKey.getName());
try {
// 停止触发器
scheduler.pauseTrigger(triggerKey);
// 移除触发器
scheduler.unscheduleJob(triggerKey);
// 删除任务
scheduler.deleteJob(jobKey);
LOG.info("----quartzJob success");
} catch (SchedulerException e) {
LOG.error("----quartzJob close exception", e);
}
}
/**
* 判断任务是否存在
*
* @param scheduler 调度器
* @param name 任务名称
* @param group 任务分组
* @return true==存在 false==不存在
*/
public static boolean exist(Scheduler scheduler, String name, String group) {
JobDetail jobDetail;
JobKey jobKey = new JobKey(name, group);
TriggerKey triggerKey = new TriggerKey(name, group);
try {
jobDetail = scheduler.getJobDetail(jobKey);
if (jobDetail != null) {
if (Trigger.TriggerState.ERROR.equals(scheduler.getTriggerState(triggerKey))) {
LOG.info("-----定时任务状态异常,恢复状态,job name :{}", name);
scheduler.resumeJob(jobKey);
}
return true;
}
} catch (SchedulerException e) {
LOG.error("-----exist exception", e);
}
return false;
}
/**
* 获取定时任务列表
*
* @param scheduler 任务调度器
* @return
*/
public static List jobs(Scheduler scheduler) {
List jobViews = Lists.newArrayList();
try {
for (String groupName : scheduler.getJobGroupNames()) {
for (JobKey jobKey : scheduler.getJobKeys(GroupMatcher.jobGroupEquals(groupName))) {
String jobName = jobKey.getName();
String jobGroup = jobKey.getGroup();
// name and group
JobView jobView = new JobView();
jobView.setName(jobName);
jobView.setGroup(jobGroup);
// params
JobDetail qJobDetail = scheduler.getJobDetail(jobKey);
if (null != qJobDetail.getJobDataMap()) {
Map params = Maps.newHashMap();
params.putAll(qJobDetail.getJobDataMap());
jobView.setParams(params);
}
List extends Trigger> triggers = scheduler.getTriggersOfJob(jobKey);
// 应该只有一个
for (Trigger trigger : triggers) {
trigger.getNextFireTime();
if (trigger instanceof CronTrigger) {
CronTrigger cronTrigger = (CronTrigger) trigger;
String cronExpr = cronTrigger.getCronExpression();
jobView.setCronExpression(cronExpr);
}
jobView.setNextFireTime(DateUtils.formatDate(trigger.getNextFireTime(), "yyyy-MM-dd HH:mm:ss"));
Trigger.TriggerState state = scheduler.getTriggerState(trigger.getKey());
jobView.setStatus(state.name());
}
jobViews.add(jobView);
}
}
} catch (Exception e) {
return Collections.emptyList();
}
return jobViews;
}
}
创建具体任务类(写需要实现的一些逻辑):
package com.test.quartz.quartz;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.JobExecutionContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.quartz.QuartzJobBean;
/**
* @author [email protected]
* @date 2019/2/13
* @description: 具体任务类
*/
@DisallowConcurrentExecution
public class TestQuartzJob extends QuartzJobBean {
private static final Logger LOGGER = LoggerFactory.getLogger(TestQuartzJob.class);
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) {
LOGGER.info("-----TestQuartzJob start");
//这里写具体的实现业务逻辑
try {
//线程睡眠是因为需要测试,防止程序过快执行
Thread.sleep(5000);
System.out.println("任务执行了" + "================================");
} catch (Exception e) {
LOGGER.error("休眠时间异常", e);
}
}
}
最后再将所有任务初始化:QuartzJobManager
package com.test.quartz.quartz;
import org.quartz.Scheduler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
/**
* @author [email protected]
* @date 2019/2/13
* @description: 初始化任务
*/
@Component
public class QuartzJobManager {
@Autowired
private Scheduler scheduler;
/**
* 初始化任务的创建
* 判断任务是否存在,不存在则创建
*/
@PostConstruct
public void initJob() {
//每分钟执行一次
QuartzJobUtils.createJob(scheduler, TestQuartzJob.class.getSimpleName(), TestQuartzJob.class,
"0 0/1 * * * ? ");
}
}
执行结果:
若是部署多台机器,到了时间点,只有一台会执行,其他不会执行。(测试方案:修改tamcat端口号,启动多台服务)
若多个节点其中一个scheduler执行job失败,将会被另外一个scheduler执行。(测试方案:修改tamcat端口号,启动多台服务,将正在执行这个任务的服务关掉)
问题:
任务节点部署在不同服务器时间同步问题(暂未解决)
代码地址:https://download.csdn.net/download/qq_33914912/10957062 (积分默认的,我想改成免费的,如果有的会的教下我去改)