基本实现原理
Quartz 核心元素
-
Scheduler-任务调度器 Scheduler由scheduler工厂创建:DirectSchedulerFactory或者StdSchedulerFactory。第二种工厂StdSchedulerFactory使用较多,因为DirectSchedulerFactory使用起来不够方便,需要作许多详细的手工编码设置。Scheduler主要有三种:RemoteMBeanScheduler,RemoteScheduler和StdScheduler。
-
Job-任务 Job用于表示被调度的任务。主要有两种类型的job:无状态的(stateless)和有状态的(stateful)。对于同一个trigger来说,有状态的job不能被并行执行,只有上一次触发的任务被执行完之后,才能触发下一次执行。Job主要有两种属性:volatility和durability,其中volatility表示任务是否被持久化到数据库存储,而durability表示在没有trigger关联的时候任务是否被保留。两者都是在值为true的时候任务被持久化或保留。一个job可以被多个trigger关联,但是一个trigger只能关联一个job。
-
Trigger-触发器 Trigger是用于定义调度时间的元素,即按照什么时间规则去执行任务。Quartz中主要提供了四种类型的trigger:SimpleTrigger,CronTirgger,DateIntervalTrigger,和NthIncludedDayTrigger。这四种trigger可以满足企业应用中的绝大部分需求。
-
Calendar 它是一些日历特定时间点的集合,如常见的节假日,可以方便的安排任务在非节假日的时候触发,一个trigger可以包含多个calendar
-
JobDetail 存储了一些Job实现类的定义的一些信息,如job的名字,组名等;在spring中有JobDetailFactoryBean和 MethodInvokingJobDetailFactoryBean两种实现,如果任务调度只需要执行某个类的某个方法,就可以通过MethodInvokingJobDetailFactoryBean来调用
-
JobDataMap 它是Map的扩展类,提供了一些便捷的方法,且可用于给Job传递参数值,如:有一个需求要给两个人发邮件,一个是发给张三,一个发给李四,不用写两个job实现类,可将其存在JobDataMap中,然后通过context在execute方法中获取到;对于同一JobDetail实例,执行多个Job实例,是共享同一个JobDataMap,也即在任务里修改了里面的值,会对其他Job实例造成影响;Trigger同样有一个JobDataMap,共享范围是所有使用这个Trigger的Job实例
-
关系图
Quartz 线程
-
Scheduler调度线程 主要有两个:执行常规调度的线程,和执行misfiredtrigger的线程。常规调度线程轮询存储的所有trigger,如果有需要触发的trigger,即到达了下一次触发的时间,则从任务执行线程池获取一个空闲线程,执行与该trigger关联的任务。Misfire线程是扫描所有的trigger,查看是否有misfiredtrigger,如果有的话根据misfire的策略分别处理(fire now OR wait for the next fire)。
-
任务执行线程 任务执行线程通常使用一个线程池维护一组线程
-
Quartz线程视图
Quartz Job数据存储
-
RAMJobStore 是将trigger和job存储在内存中,存取速度非常快,但是由于其在系统被停止后所有的数据都会丢失
-
JobStoreSupport 是基于jdbc将trigger和job存储到数据库中,在集群应用中,必须使用JobStoreSupport
集群架构
Quartz 集群架构
一个Quartz集群中的每个节点是一个独立的Quartz应用,它又管理着其他的节点。这就意味着你必须对每个节点分别启动或停止。Quartz集群中,独立的Quartz节点并不与另一其的节点或是管理节点通信,而是通过相同的数据库表来感知到另一Quartz应用的,如图:
Quartz 集群数据库表
| 表名 | 说明 |
|---|---|
| QRTZ_CALENDARS | 以 Blob 类型存储 Quartz 的 Calendar 信息 |
| QRTZ_CRON_TRIGGERS | 存储 Cron Trigger,包括 Cron表达式和时区信息 |
| QRTZ_FIRED_TRIGGERS | 存储与已触发的 Trigger 相关的状态信息,以及相联 Job的执行信息 |
| QRTZ_PAUSED_TRIGGER_GRPS | 存储已暂停的 Trigger 组的信息 |
| QRTZ_SCHEDULER_STATE | 存储少量的有关 Scheduler 的状态信息,和别的 Scheduler实例(假如是用于一个集群中) |
| QRTZ_LOCKS | 存储程序的悲观锁的信息(假如使用了悲观锁) |
| QRTZ_JOB_DETAILS | 存储每一个已配置的 Job 的详细信息 |
| QRTZ_JOB_LISTENERS | 存储有关已配置的 JobListener 的信息 |
| QRTZ_SIMPLE_TRIGGERS | 存储简单的Trigger,包括重复次数,间隔,以及已触的次数 |
| QRTZ_BLOG_TRIGGERS | Trigger 作为 Blob 类型存储(用于 Quartz 用户用 JDBC创建他们自己定制的 Trigger 类型,JobStore 并不知道如何存储实例的时候) |
| QRTZ_TRIGGER_LISTENERS | 存储已配置的 TriggerListener 的信息 |
| QRTZ_TRIGGERS | 存储已配置的 Trigger 的信息 |
Quartz 表字段解释
- 任务详细信息表(qr****tz_job_details) 说明:保存job详细信息,该表需要用户根据实际情况初始化
jo****b_name:集群中job的名字,该名字用户自己可以随意定制,无强行要求。 job_g****roup:集群中job的所属组的名字,该名字用户自己随意定制,无强行要求。 job_class_name:集群中job实现类的完全包名,quartz就是根据这个路径到classpath找到该job类的。 is_durable:是否持久化,把该属性设置为1,quartz会把job持久化到数据库中 job_data:一个blob字段,存放持久化job对象。
-
触发器信息表(qrtz_triggers) trigger_name:trigger的名字,该名字用户自己可以随意定制,无强行要求 trigger_group:trigger所属组的名字,该名字用户自己随意定制,无强行要求 job_name:qrtz_job_details表job_name的外键 job_group:qrtz_job_details表job_group的外键 trigger_state:当前trigger状态设置为ACQUIRED,如果设为WAITING,则job不会触发 trigger_cron:触发器类型,使用cron表达式
-
表qrtz_cron_triggers:存储cron表达式表 trigger_name: qrtz_triggers表trigger_name的外键
trigger_group: qrtz_triggers表trigger_group的外键
cron_expression:cron表达式
-
触发器与任务关联表(qrtz_fired_triggers) 存储与已触发的Trigger相关的状态信息,以及相联Job的执行信息。
-
调度器状态表(qrtz_scheduler_state) **说明:**集群中节点实例信息,Quartz定时读取该表的信息判断集群中每个实例的当前状态
instance_name:之前配置文件中org.quartz.scheduler.instanceId配置的名字,就会写入该字段,如果设置为AUTO,quartz会根据物理机名和当前时间产生一个名字
last_checkin_time:上次检查时间
checkin_interval:检查间隔时间
Quartz启动流程
若quartz是配置在spring中,当服务启动时,就会装载相关的bean;SchedulerFactoryBean实现了InitializingBean接口,故在初始化bean的时候,会执行afterPropertiesSet方法,该方法再调用ScheduerFactory(一般是StdSchedulerFactory)创建Scheduler.SchedulerFactory在创建quartzScheduer的过程中,将会读取配置参数,初始化各个组件,关键组件如下:
ThreadPool:一般是使用SimpleThreadPool,SimpleThreadPool创建了一定数量的WorkerThread实例来使得Job能够在线程中进行处理。WorkerThread是定义在SimpleThreadPool类中的内部类,它实质上就是一个线程。在SimpleThreadPool中有三个list:workers-存放池中所有的线程引用,availWorkers-存放所有空闲的线程,busyWorkers-存放所有工作中的线程;
JobStore:分为存储在内存的RAMJobStore和存储在数据库的JobStoreSupport(包括JobStoreTX和JobStoreCMT两种实现,JobStoreCMT是依赖于容器来进行事务的管理,而JobStoreTX是自己管理事务),若要使用集群要使用JobStoreSupport的方式;
QuartzSchedulerThread:用来进行任务调度的线程,在初始化的时候paused=true,halted=false,虽然线程开始运行了,但是paused=true,线程会一直等待,直到start方法将paused置为false;
另外,SchedulerFactoryBean还实现了SmartLifeCycle接口,因此初始化完成后,会执行start()方法,该方法将主要会执行以下的几个动作:
创建ClusterManager线程并启动线程:该线程用来进行集群故障检测和处理;
创建MisfireHandler线程并启动线程:该线程用来进行misfire任务的处理;
置QuartzSchedulerThread的paused=false,调度线程才真正开始调度;
侦测失败的Scheduler节点
当一个Scheduler实例执行检入时,它会查看是否有其他的Scheduler实例在到达他们所预期的时间还未检入。这是通过检查SCHEDULER_STATE表中Scheduler记录在LAST_CHEDK_TIME列的值是否早于org.quartz.jobStore.clusterCheckinInterval来确定的。如果一个或多个节点到了预定时间还没有检入,那么运行中的Scheduler就假定它(们) 失败了。
从故障实例中恢复Job
当一个Sheduler实例在执行某个Job时失败了,有可能由另一正常工作的Scheduler实例接过这个Job重新运行。要实现这种行为,配置给JobDetail对象的Job可恢复属性必须设置为true(job.setRequestsRecovery(true))。如果可恢复属性被设置为false(默认为false),当某个Scheduler在运行该job失败时,它将不会重新运行;而是由另一个Scheduler实例在下一次触发时间触发。Scheduler实例出现故障后多快能被侦测到取决于每个Scheduler的检入间隔(即2.3中提到的org.quartz.jobStore.clusterCheckinInterval)。
实例
application-quartz.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns...>
<!-- 注册集群调度任务 -->
<bean id="recipeManageSchedule" lazy-init="false" autowire="no" class="org.springframework.scheduling.quartz.SchedulerFactoryBean" destroy-method="destroy">
<!-- 数据源 -->
<property name="dataSource" ref="dataSource" />
<!-- 可选,QuartzScheduler 启动时更新己存在的Job,这样就不用每次修改targetObject后删除qrtz_job_details表对应记录了 -->
<property name="overwriteExistingJobs" value="true" />
<!-- 必须的,QuartzScheduler 延时启动,应用启动完后 QuartzScheduler 再启动 -->
<property name="startupDelay" value="3" />
<!-- 设置自动启动 -->
<property name="autoStartup" value="true" />
<property name="applicationContextSchedulerContextKey" value="applicationContext" />
<property name="configLocation" value="classpath:quartz.properties" />
</bean>
</beans
quartz.properties
#==============================================================
#Configure Main Scheduler Properties
#==============================================================
org.quartz.scheduler.instanceName = quartzScheduler
org.quartz.scheduler.instanceId = AUTO
#==============================================================
#Configure JobStore
#==============================================================
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.tablePrefix = QRTZ_
org.quartz.jobStore.isClustered = true
org.quartz.jobStore.clusterCheckinInterval = 10000
org.quartz.jobStore.dataSource = myDS
#==============================================================
#Configure DataSource
#==============================================================
org.quartz.dataSource.myDS.driver = com.mysql.jdbc.Driver
org.quartz.dataSource.myDS.URL = jdbc:mysql://192.168.31.18:3306/test?useUnicode=true&characterEncoding=UTF-8
org.quartz.dataSource.myDS.user = root
org.quartz.dataSource.myDS.password = 123456
org.quartz.dataSource.myDS.maxConnections = 30
#==============================================================
#Configure ThreadPool
#==============================================================
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 5
org.quartz.threadPool.threadPriority = 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true
ScheduleFactory
import com.ruhnn.dao.QrtzJobDetailDAO;
import com.ruhnn.error.CombException;
import com.ruhnn.error.ErrorCode;
import com.ruhnn.model.JobDetailDO;
import org.quartz.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Resource;
import java.util.Date;
import java.util.HashSet;
/**
* @author 郭大海
*/
public class ScheduleFactory {
@Resource
private Scheduler combCenterSchedulerBean
@Resource
private QrtzJobDetailDAO qrtzJobDetailDAO;
}
/**
* 添加任务
*/
public boolean addJob(RuhnnJobDetail ruhnnJobDetail) throws SchedulerException {
TriggerKey triggerKey = TriggerKey.triggerKey(ruhnnJobDetail.getTaskName(), ruhnnJobDetail.getGroupName());
JobKey jobKey = new JobKey(ruhnnJobDetail.getTaskName(), ruhnnJobDetail.getGroupName());
if (existJob(ruhnnJobDetail.getTaskName())) {
return false;
}
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(ruhnnJobDetail.getCron()).withMisfireHandlingInstructionDoNothing();
CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity(triggerKey).withSchedule(cronScheduleBuilder).build();
JobDetail jobDetail = JobBuilder.newJob(ExecuteJob.class).storeDurably(true).usingJobData(ruhnnJobDetail.getDataMap()).withIdentity(jobKey).withDescription(ruhnnJobDetail.getDesc()).build();
Date date = combCenterSchedulerBean.scheduleJob(jobDetail, cronTrigger);
return true;
}
/**
* 立即执行任务
*/
public boolean executeImmediately(String groupName, String taskName) throws SchedulerException,CombException {
if (!existJob(taskName)) {
throw new CombException(ErrorCode.ERROR_PARAMETER,"当前任务不存在,当前任务不存在");
}
JobKey jobKey = new JobKey(taskName, groupName);
combCenterSchedulerBean.triggerJob(jobKey);
return true;
}
}
/**
* 删除任务
*/
public boolean deleteJob(String groupName, String taskName) throws SchedulerException {
TriggerKey tk = TriggerKey.triggerKey(taskName, groupName);
combCenterSchedulerBean.pauseTrigger(tk);
combCenterSchedulerBean.unscheduleJob(tk);
JobKey jobKey = JobKey.jobKey(taskName, groupName);
combCenterSchedulerBean.deleteJob(jobKey);
return true;
}
public boolean existJob(String taskName) throws SchedulerException {
JobDetailDO jobDetailDO = qrtzJobDetailDAO.queryOne(getSchedulerName(), taskName);
return jobDetailDO != null;
}
/**
* 修改任务
*/
public boolean rescheduleJob(String groupName, String taskName, String cron) throws SchedulerException {
if (!existJob(taskName)) {
return false;
}
TriggerKey triggerKey = TriggerKey.triggerKey(taskName, groupName);
CronTrigger oldTrigger = (CronTrigger) combCenterSchedulerBean.getTrigger(triggerKey);
if (oldTrigger != null) {
String oldCron = oldTrigger.getCronExpression();
if (oldCron.equals(cron)) {
return true;
}
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cron).withMisfireHandlingInstructionDoNothing();
oldTrigger = oldTrigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(cronScheduleBuilder).build();
combCenterSchedulerBean.rescheduleJob(triggerKey, oldTrigger);
} else {
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cron).withMisfireHandlingInstructionDoNothing();
CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity(triggerKey).withSchedule(cronScheduleBuilder).build();
JobKey jobKey = new JobKey(taskName, groupName);
JobDetail jobDetail = combCenterSchedulerBean.getJobDetail(jobKey);
HashSet<Trigger> triggerSet = new HashSet<>();
triggerSet.add(cronTrigger);
combCenterSchedulerBean.scheduleJob(jobDetail, triggerSet, true);
}
return true;
}
/**
* 暂停任务
*/
public boolean pauseJob(String groupName, String taskName) throws SchedulerException {
TriggerKey triggerKey = TriggerKey.triggerKey(taskName, groupName);
if (existJob(taskName)) {
combCenterSchedulerBean.pauseTrigger(triggerKey);
return true;
}
return false;
}
/**
* 恢复任务
*/
public boolean resumeJob(String groupName, String taskName) throws SchedulerException {
TriggerKey triggerKey = TriggerKey.triggerKey(taskName, groupName);
if (existJob(taskName)) {
combCenterSchedulerBean.resumeTrigger(triggerKey);
return true;
}
return false;
}
/**
* 获取 trigger 状态
*/
public Trigger.TriggerState getTriggerStatus(String groupName, String taskName) throws SchedulerException{
TriggerKey triggerKey = TriggerKey.triggerKey(taskName, groupName);
if (existJob(taskName)) {
return combCenterSchedulerBean.getTriggerState(triggerKey);
}
return null;
}
/**
* 更新任务
*/
public boolean rescheduleJob(RuhnnJobDetail ruhnnJobDetail) throws SchedulerException {
String taskName = ruhnnJobDetail.getTaskName();
String groupName = ruhnnJobDetail.getGroupName();
TriggerKey triggerKey = TriggerKey.triggerKey(taskName, groupName);
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(ruhnnJobDetail.getCron()).withMisfireHandlingInstructionDoNothing();
CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity(triggerKey).withSchedule(cronScheduleBuilder).build();
HashSet<Trigger> triggerSet = new HashSet<>();
triggerSet.add(cronTrigger);
JobKey jobKey = JobKey.jobKey(taskName, groupName);
JobDetail jobDetail = JobBuilder.newJob(ExecuteJob.class).storeDurably(true).usingJobData(ruhnnJobDetail.getDataMap()).withIdentity(jobKey).withDescription(ruhnnJobDetail.getDesc()).build();
combCenterSchedulerBean.scheduleJob(jobDetail, triggerSet, true);
return true;
}
public String getSchedulerName() throws SchedulerException {
return combCenterSchedulerBean.getSchedulerName();
}
public void destroy() {
try {
if (!combCenterSchedulerBean.isShutdown()) {
combCenterSchedulerBean.shutdown();
}
} catch (Exception e) {
e.printStackTrace();
}
}
public void start() {
try {
if (!combCenterSchedulerBean.isStarted()) {
combCenterSchedulerBean.start();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
Job
import com.ruhnn.constants.CombTaskConstants;
import com.ruhnn.task.route.RuhnnJob;
import com.ruhnn.task.schedule.RuhnnJobApplication;
import org.quartz.*;
/**
* @author guodahai
*/
@PersistJobDataAfterExecution
@DisallowConcurrentExecution
public class ExecuteJob implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
JobDataMap jobDataMap = jobExecutionContext.getJobDetail().getJobDataMap();
RuhnnJob ruhnnJob = (RuhnnJob) jobDataMap.get(CombTaskConstants.JOB_DETAIL);
//处理任务链
RuhnnJobApplication.handlerManager.handler(ruhnnJob);
}
}
注意事项
SchedulerFactory无法注入的问题
在JobTaskServiceImpl中不要直接将recipeManageSchedule直接注入,那样会报空;它是一个工厂类,得到的不是它本身,而是它负责创建的org.quartz.impl.StdScheduler对象,只要把Service中的recipeManageSchedule用Scheduler替换即可;
时间同步的问题:
Quartz实际并不关心你是在相同还是不同的机器上运行节点。当集群放置在不同的机器上时,称之为水平集群。节点跑在同一台机器上时,称之为垂直集群。对于垂直集群,存在着单点故障的问题。这对高可用性的应用来说是无法接受的,因为一旦机器崩溃了,所有的节点也就被终止了。对于水平集群,存在着时间同步问题。
节点用时间戳来通知其他实例它自己的最后检入时间。假如节点的时钟被设置为将来的时间,那么运行中的Scheduler将再也意识不到那个结点已经宕掉了。另一方面,如果某个节点的时钟被设置为过去的时间,也许另一节点就会认定那个节点已宕掉并试图接过它的Job重运行。最简单的同步计算机时钟的方式是使用某一个Internet时间服务器(Internet Time Server ITS)。
quartz mysql死锁问题
quartz文档建议我们在集群环境下,最好将org.quartz.jobStore.txIsolationLevelSerializable设置为true;这个选项在mysql下非常容易出现死锁
这个选项的作用:
quartz需要提升隔离级别来保障自己的运作,不过,由于各数据库实现的隔离级别定义都不一样,所以quartz提供一个设置序列化这样的隔离级别存在,因为例如oracle中是没有未提交读和可重复读这样的隔离级别存在。但是由于mysql默认的是可重复读,比提交读高了一个级别,所以已经可以满足quartz集群的正常运行。