前言
什么是集群?
集群指的是将几台服务器集中在一起,实现同一业务.集群则是通过提高单位时间内执行的任务数来提升效率
什么分布式?
分布式是指将不同的业务分布在不同的地方,分布式是以缩短单个任务的执行时间来提升效率的
组件
quartz需要配置的组件
1)JobDetail, Trigger ,Scheduler
2)线程池(每一次任务开一个新线程去执行)
3)数据源DataSources 及db表
4) quartz配置(主Scheduler,jobstore配置)
Table
Quartz集群中,独立的Quartz节点并不与另一其的节点或是管理节点通信,而是通过相同的数据库表来感知到另一Quartz应用,而且通过数据库表行级锁来实现同一时间段只有一个quartz节点进行操作。
在github上找到托管项目
在目录quartz-quartz-2.3.0\quartz-core\src\main\resources\org\quartz\impl\jdbcjobstore 可以找到建表的sql
(版本不同目录结构可能不同)
jar
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz-jobs</artifactId>
<version>2.3.0</version>
</dependency>
application.xml
配置数据源
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-4.1.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.1.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.1.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.1.xsd ">
<context:property-placeholder location="classpath:db.properties" />
<import resource="classpath:spring/spring-quartz.xml" />
<!-- 数据库连接池 -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<property name="maxActive" value="10"/>
<property name="maxIdle" value="5"/>
</bean>
</beans>
spring-quartz.xml
配置线程池,JobDetail,Trigger,Schedual
配置关系图
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:cache="http://www.springframework.org/schema/cache"
xmlns:task="http://www.springframework.org/schema/task"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-4.1.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.1.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.1.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.1.xsd
http://www.springframework.org/schema/cache
http://www.springframework.org/schema/cache/spring-cache-4.1.xsd
http://www.springframework.org/schema/task
http://www.springframework.org/schema/task/spring-task-4.1.xsd ">
<context:component-scan base-package="com.frame.springmvc.task" />
<!-- 配置定时任务线程池 -->
<bean id="quartzExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<property name="corePoolSize" value="10"/>
<property name="maxPoolSize" value="100"/>
<property name="keepAliveSeconds" value="300"/>
<property name="queueCapacity" value="500"/>
</bean>
<!-- 把 Quartz 的 Task 实例化进入数据库时,会产生: Serializable 的错误 -->
<!-- 这个 MethodInvokingJobDetailFactoryBean 类中的 methodInvoking 方法,是不支持序列化的,因此在把 QUARTZ 的 TASK 序列化进入数据库时就会抛错。 -->
<!-- 调度业务 定义一个JobDetai-->
<!-- <bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject" ref="expireJobTask" />
<property name="targetMethod" value="run" />
</bean> -->
<!-- 定义JobDetail -->
<bean id="printTimeJobDetail" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
<!-- durability 表示任务完成之后是否依然保留到数据库,默认false-->
<property name="durability" value="true"/>
<!--当Quartz服务被中止后,再次启动或集群中其他机器接手任务时会尝试恢复执行之前未完成的所有任务-->
<property name="requestsRecovery" value="true" />
<property name="jobClass" value="com.frame.springmvc.task.utils.MyDetailQuartzJobBean" />
<property name="description" value="打印时间定时器"/>
<property name="jobDataAsMap">
<map>
<entry key="targetObject" value="printTimeQuartz"/>
<entry key="targetMethod" value="execute"/>
</map>
</property>
</bean>
<!-- 定义一个Trigger -->
<bean id="printTimeCronTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<property name="jobDetail" ref="printTimeJobDetail" />
<property name="cronExpression" value="0/20 * * * * ?" /> <!-- 秒 分 小时 日 月 星期 年 -->
</bean>
<!-- 该调度表示,延迟10秒启动,然后每隔1分钟执行一次 定义一个Trigger -->
<!--<bean id="taskTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerBean">
<property name="jobDetail" ref="jobDetail" />
<property name="startDelay" value="10000" />
<property name="repeatInterval" value="60000" />
</bean> -->
<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="configLocation" value="classpath:quartz.properties"/>
<property name="dataSource" ref="dataSource"/>
<property name="triggers">
<list>
<ref bean="printTimeCronTrigger" />
</list>
</property>
<property name="applicationContextSchedulerContextKey" value="applicationContext"/>
<!-- 线程 -->
<property name="taskExecutor" ref="quartzExecutor" />
<!--用于quartz集群,QuartzScheduler 启动时更新己存在的Job,这样就不用每次修改targetObject后删除qrtz_job_details表对应记录了--> <!--每台集群机器部署应用的时候会更新触发器-->
<!--更新trigger的 表达式时,同步数据到数据库qrtz_cron_triggers表 开启-->
<property name="overwriteExistingJobs" value="true"/>
<!--QuartzScheduler 延时启动 应用启动完5秒后 QuartzScheduler 再启动-->
<property name="startupDelay" value="5"/>
</bean>
</beans>
1.同步更新
更新Trigger或JobDetail后需要将数据更新到数据库表中 需要开启 overwriteExistingJobs为true
2.Spring从2.0.2开始便不再支持Quartz。这个 MethodInvokingJobDetailFactoryBean 类中的 methodInvoking 方法,是不支持序列化的,因此在把 QUARTZ 的 TASK 序列化进入数据库时就会抛错。
解决方案:使用org.springframework.scheduling.quartz.JobDetailFactoryBean来产生JobDetail
quartz.properties
主要对JobStore进行配置,开启集群功能
#org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
#============================================================================
# Configure Main Scheduler Properties
# Needed to manage cluster instances
#============================================================================
#默认主机名和时间戳生成实例ID,可以是任何字符串,但对于所有调度程序来说,必须是唯一的
org.quartz.scheduler.instanceId=AUTO
#仅当org.quartz.scheduler.instanceId设置为“AUTO” 时才使用
#org.quartz.scheduler.instanceIdGenerator.class
#可以是任何字符串,并且该值对调度程序本身没有意义,而是作为客户端代码在同一程序中使用多个实例时区分调度程序的机制。如果您正在使用群集功能,则必须对群集中“逻辑上”相同的调度程序的每个实例使用相同的名称。
#org.quartz.scheduler.instanceName=QuartzScheduler 默认
####RMI功能####
#如果您希望Quartz Scheduler通过RMI作为服务器导出本身,则将“rmi.export”标志设置为true。
org.quartz.scheduler.rmi.export = false
#如果要连接(使用)远程服务的调度程序,则将“org.quartz.scheduler.rmi.proxy”标志设置为true。您还必须指定RMI注册表进程的主机和端口 - 通常是“localhost”端口1099。
org.quartz.scheduler.rmi.proxy = false
#org.quartz.scheduler.rmi.registryHost
#org.quartz.scheduler.rmi.registryPort
#============================================================================
# Configure JobStore
#============================================================================
#default config
#org.quartz.jobStore.class: org.quartz.simpl.RAMJobStore
#持久化配置
org.quartz.jobStore.class:org.quartz.impl.jdbcjobstore.JobStoreTX
#我们仅为数据库制作了特定于数据库的代理
org.quartz.jobStore.driverDelegateClass:org.quartz.impl.jdbcjobstore.StdJDBCDelegate
#以指示JDBCJobStore将JobDataMaps中的所有值都作为字符串,因此可以作为名称 - 值对存储而不是在BLOB列中以其序列化形式存储更多复杂的对象。从长远来看,这是更安全的,因为您避免了将非String类序列化为BLOB的类版本问题。
org.quartz.jobStore.useProperties:true
#数据库表前缀
org.quartz.jobStore.tablePrefix:qrtz_
#数据库名字
#org.quartz.jobStore.dataSource:qzDS
#打开群集功能 ,Clustering目前与JDBC-Jobstore(JobStoreTX或JobStoreCMT)和TerracottaJobStore一起使用。功能包括负载平衡和 job故障转移(如果JobDetail的“请求恢复”标志设置为true)。
org.quartz.jobStore.isClustered:true
#设置此实例“检入”*与群集的其他实例的频率(以毫秒为单位)。影响检测失败实例的速度。
org.quartz.jobStore.clusterCheckinInterval = 5000
#在被认为“失火”之前,调度程序将“容忍”一个Triggers将其下一个启动时间通过的毫秒数。默认值(如果您在配置中未输入此属性)为60000(60秒)。
org.quartz.jobStore.misfireThreshold = 60000
QuartzJobBean
一个JobBean执行类,通过JobDetailFactoryBean将targetObject和targetMethod值注入,通过spring容器获得bean对象并执行相应任务方法
package com.frame.springmvc.task.utils;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.quartz.QuartzJobBean;
import com.frame.springmvc.utils.DebugLogger;
public class MyDetailQuartzJobBean extends QuartzJobBean{
/**
* 目标对象
*/
private String targetObject;
/**
* 执行的方法
*/
private String targetMethod;
/**
* spring 上下文对象
*/
private ApplicationContext ctx;
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
try {
DebugLogger.log("execute [" + targetObject + "] at once>>>>>>");
Object otargetObject = ctx.getBean(targetObject);
Method m = otargetObject.getClass().getMethod(targetMethod, new Class[] {});
m.invoke(otargetObject, new Object[]{});
} catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
e.printStackTrace();
}
}
public void setApplicationContext(ApplicationContext applicationContext) {
this.ctx = applicationContext;
}
public void setTargetObject(String targetObject) {
this.targetObject = targetObject;
}
public void setTargetMethod(String targetMethod) {
this.targetMethod = targetMethod;
}
}
Job
需要使用@Service注解来扫描注入Spring容器
package com.frame.springmvc.task;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.springframework.stereotype.Service;
import com.frame.springmvc.utils.DebugLogger;
/**
* job 实现类需要需要序列化(存入db)
*/
@Service("printTimeQuartz")
public class PrintTimeQuartz implements Serializable{
/**
*
*/
private static final long serialVersionUID = -2057846704016861663L;
public void execute(){
SimpleDateFormat simpleDateFormat =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//打印当前时间
DebugLogger.log("Quartz ExpireJobTask.run()... : Current time is :"+simpleDateFormat.format(new Date()));
}
}
quartz是默认多线程并发,对于一个quartz节点。A时刻触发线程一执行job,A+interval 时刻触发线程二执行job ,如果线程一还未执行完成,线程二开始执行,
如果Job是一个同步任务。此时会有问题。
解决办法:对job的execute加同步锁
运行
使用Intellij Idea 模拟两个server启动,server1和server2
server1:
问题
1.观察server1 log
1)第一次quartz执行时间并没有按照trigger的cron表达式执行
2)quartz执行了2次
2.原因:quartz中延迟但不超时的任务在获得可用线程时立即执行
M时刻: QuartzScheduler执行时间计划
K时刻:spring启动完成
S时刻:Trigger开始执行,并打印log
(S-K):延迟时间
(S-M):触发器超时时间
(S-M)时间段存在2个延迟任务,所以在S时刻获得线程时立即执行所有未超时的延迟任务,关于超时任务的处理要视cronTrigger的超时处理策略(misfireInstruction)
打个比方:
我设置15s的情况下,如果我在10:00:00执行了一个任务,下次就是15s,但是如果服务器挂掉,我在10:00:50重新启动服务器,由于任务间隔是15s,所以从00-50s中间会有三个任务超时没有执行(本来应该分别是在15s,30s,45s执行),而延迟的时间均小于1min,所以服务器启动以后会立即开启三个quartz线程来执行延迟的任务。
3.解决
设置cronTrigger超时处理策略为 MISFIRE_INSTRUCTION_DO_NOTHING(2),并缩短临界时间(misfireThreshold)
1)crontrigger超时处理策略?
MISFIRE_INSTRUCTION_FIRE_ONCE_NOW (1) 默认
是否超过临界时间都会首选执行一次,再按正常计划执行
MISFIRE_INSTRUCTION_DO_NOTHING (2)
超过临界时间后,超时任务不在执行
<!-- 定义一个Trigger -->
<bean id="printTimeCronTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<property name="jobDetail" ref="printTimeJobDetail" />
<property name="cronExpression" value="0/5 * * * * ?" /> <!-- 秒 分 小时 日 月 星期 年 -->
<property name="misfireInstruction" value="2"></property>
</bean>
2)什么是misfireThreshold?
在quartz.properties中,我们配置misfireThreshold属性为60秒,用来设置触发器超时的临界时间
#在被认为“失火”之前,调度程序将“容忍”一个Triggers将其下一个启动时间通过的毫秒数。默认值(如果您在配置中未输入此属性)为60000(60秒)。
org.quartz.jobStore.misfireThreshold = 60000
3)什么是触发器超时?
比如任务A在2点执行,因为线程忙碌,在2点05分获得可用线程,其中这5秒为触发器超时时间