一、前言
使用Quartz框架执行定时任务时,若是将应用部署到多台服务器,那么到了设置的时间点,多台服务器便会同时都在执行定时任务,这不符合我们预期的功能。按照我们的预期是:只有1台服务器在执行定时任务,其他服务器为备用服务器,当其中一台服务器出现故障时,Quartz自动进行故障转移failover,不用人为进行干预,就自动由另一台服务器来执行定时任务,这就是我们使用Quartz集群的目的。
Quartz集群支持多种方式,本文中主要介绍Quartz+Mysql的实现方式,在Quartz集群中每一个Scheduler实例都是一个节点,各个节点之间没有直接的通信,而是通过定时访问同一个数据库的方式来实现集群。每个节点在访问数据库时会更新实例的状态并检测节点的健康状态,每个节点会定时(org.quartz.jobStore.clusterCheckinInterval配置属性的时间,默认是7500毫秒)更新qrtz_scheduler_state数据表中的对应节点的LAST_CHECKIN_TIME字段,同时会遍历集群中是否有其他节点在到达他们预期的时间还未CHECKIN,则认为该节点故障,集群管理线程检测到故障节点,就会更新触发器的状态,从而达到故障转移的功能。
二、创建数据表
从Quartz的jar包org.quartz.impl.jdbcjobstore目录下取出tables_mysql_innodb.sql,然后在mysql中执行该sql脚本
执行成功后会生成11张数据表
三、创建Spring Boot工程
四、修改POM文件
在pom.xml文件中添加c3p0
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>
Quartz默认是使用c3p0数据库连接池,如果缺少c3p0库,系统会报异常信息:ConnectionProvider class 'org.quartz.utils.C3p0PoolingConnectionProvider' could not be instantiated。
五、创建OrderJob和QuartzConfiguration类
5.1 OrderJob类
package com.ljhua.quartz3;
import org.quartz.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@PersistJobDataAfterExecution
@DisallowConcurrentExecution
public class OrderJob implements Job {
Logger log = LoggerFactory.getLogger(this.getClass());
private static final String NUM_EXECUTIONS = "NumExecutions";
@Override
public void execute(JobExecutionContext context) {
int executeCount = 0;
JobKey jobKey = context.getJobDetail().getKey();
JobDataMap map = context.getJobDetail().getJobDataMap();
if (map.containsKey(NUM_EXECUTIONS)) {
executeCount = Integer.valueOf(map.getString(NUM_EXECUTIONS));
}
executeCount++;
map.put(NUM_EXECUTIONS, String.valueOf(executeCount));
log.info("{}-处理逻辑-start:*** {}", jobKey, executeCount);
log.info("{}-处理逻辑-end:*** {}", jobKey, executeCount);
}
}
5.2 QuartzConfiguration类
package com.ljhua.quartz3;
import org.quartz.*;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import static org.quartz.CronScheduleBuilder.cronSchedule;
import static org.quartz.TriggerBuilder.newTrigger;
@Configuration
@AutoConfigureAfter({SchedulerFactoryBean.class})
public class QuartzConfiguration {
public QuartzConfiguration(SchedulerFactoryBean quartzScheduler) throws SchedulerException {
Scheduler sched = quartzScheduler.getScheduler();
TriggerKey triggerKey1 = TriggerKey.triggerKey("trigger1", "group1");
JobDetail job1 = JobBuilder.newJob(OrderJob.class).withIdentity("job1", "group1").build();
CronTrigger trigger1 = newTrigger().withIdentity(triggerKey1).withSchedule(cronSchedule("0/5 * * * * ?")).build();
if (sched.checkExists(triggerKey1)) {
sched.rescheduleJob(triggerKey1, trigger1);
} else {
sched.scheduleJob(job1, trigger1);
}
}
}
在Quartz集群模式下,Trigger和JobDetail是保存在数据库中的,只有在项目中有增加了新的Trigger和JobDetail时才调用sched.scheduleJob()方法,所以在QuartzConfiguration类中要做条件判断sched.checkExists(triggerKey1),不能每次程序启动时都调用sched.scheduleJob(job1, trigger1),否则系统会抛出异常信息:Unable to store Job : 'group1.job1', because one already exists with this identification。
调用sched.rescheduleJob(triggerKey1, trigger1)会更新qrtz_cron_triggers数据表中的cron_expression
调用sched.addJob(job1, true, true)会把JobDetail更新到qrtz_job_details数据表中,同时也会影响到JobDetail中的JobDataMap数据,一般情况下不需要用到ached.addJob()
六、配置属性
6.1 配置文件application.yml
spring:
quartz:
properties:
org:
quartz:
scheduler:
instanceName: TestScheduler
instanceId: AUTO
threadPool:
class: org.quartz.simpl.SimpleThreadPool
threadCount: 10
threadPriority: 5
jobStore:
misfireThreshold: 60000
class: org.quartz.impl.jdbcjobstore.JobStoreTX
driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
useProperties: true
tablePrefix: qrtz_
isClustered: true
dataSource: myDS
clusterCheckinInterval: 5000
dataSource:
myDS:
driver: com.mysql.cj.jdbc.Driver
URL: jdbc:mysql://127.0.0.1:3306/ljh?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&allowMultiQueries=true&useAffectedRows=true&verifyServerCertificate=false&useSSL=false
user: root
password: 123456
maxConnections: 5
validationQuery: select 0
6.2 配置属性org.quartz.jobStore.userProperties
userProperties默认配置为false,设置为true时表示JobDataMap中的value存放的类型必须是String类型,存放非字符串时会抛出一个异常信息:Couldn't serialize job data: JobDataMap values must be Strings when the 'useProperties' property is set。
在OrderJob类中演示了用JobDataMap存储数据记录Job的执行次数,并没有使用Integer类型而是String类型。
实际应用中最好不要在userProperties=false条件下使用JobDataMap,会出现Job假死,但是qrtz_scheduler_state数据表中的LAST_CHECKIN_TIME字段却不断更新,导致故障转移failover无效,Quartz的集群作用没有发挥出来。
6.3 配置属性org.quartz.scheduler.instanceId
instanceId属性配置的是集群中节点的名称,为了给每个节点一个不同的名称,而又不需要像下面这样来传参:
java -jar target\quartz3-0.0.1-SNAPSHOT.jar --spring.quartz.properties.org.quartz.scheduler.instanceId=ONE
java -jar target\quartz3-0.0.1-SNAPSHOT.jar --spring.quartz.properties.org.quartz.scheduler.instanceId=TWO
org.quartz.scheduler.instanceId配置等于AUTO时,系统会为每个Scheduler实例自动生成一个节点名称,而不是节点的名称都是AUTO,直接这样运行就可以了
java -jar target\quartz3-0.0.1-SNAPSHOT.jar
即使是在同一台主机运行2个Scheduler实例,节点的名称也是不同的,从qrtz_scheduler_state数据表中的INSTANCE_NAME字段可以看出来