向大家推荐一篇文章:http://www.cnblogs.com/davidwang456/p/4205237.html
介绍的非常详细。
一、核心概念 Quartz的原理不是很复杂,只要搞明白几个概念,然后知道如何去启动和关闭一个调度程序即可。 1、Job 表示一个工作,要执行的具体内容。此接口中只有一个方法 void execute(JobExecutionContext context) 2、JobDetail JobDetail表示一个具体的可执行的调度程序,Job是这个可执行程调度程序所要执行的内容,另外JobDetail还包含了这个任务调度的方案和策略。 3、Trigger代表一个调度参数的配置,什么时候去调。 4、Scheduler代表一个调度容器,一个调度容器中可以注册多个JobDetail和Trigger。当Trigger与JobDetail组合,就可以被Scheduler容器调度了。使用 Quartz 需要数据库内建立11张表,如下。建表顺序需要注意。因为有的表有外键,
/*DDL 信息*/------------ # 建表顺序 1 以 Blob 类型存储 Quartz 的 Calendar 信息 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`) ) ENGINE=INNODB DEFAULT CHARSET=utf8 # 建表顺序 2 存储程序的悲观锁的信息(假如使用了悲观锁) CREATE TABLE `qrtz_locks` ( `SCHED_NAME` VARCHAR(120) NOT NULL COMMENT '方案名称', `LOCK_NAME` VARCHAR(40) NOT NULL COMMENT '程序对方案使用的锁', PRIMARY KEY (`SCHED_NAME`,`LOCK_NAME`) ) ENGINE=INNODB DEFAULT CHARSET=utf8 # 建表顺序 3 CREATE TABLE `qrtz_paused_trigger_grps` ( `SCHED_NAME` VARCHAR(120) NOT NULL, `TRIGGER_GROUP` VARCHAR(200) NOT NULL, PRIMARY KEY (`SCHED_NAME`,`TRIGGER_GROUP`) ) ENGINE=INNODB DEFAULT CHARSET=utf8 # 建表顺序 4 存储与已触发的 Trigger 相关的状态信息,以及相联 Job 的执行信息 CREATE TABLE `qrtz_fired_triggers` ( `SCHED_NAME` VARCHAR(120) NOT NULL COMMENT '方案名称', `ENTRY_ID` VARCHAR(95) NOT NULL COMMENT '执行方案的 实体唯一编号', `TRIGGER_NAME` VARCHAR(200) NOT NULL COMMENT '定时器名称', `TRIGGER_GROUP` VARCHAR(200) NOT NULL COMMENT '定时器分组', `INSTANCE_NAME` VARCHAR(200) NOT NULL COMMENT '执行定时器的设备实例名称', `FIRED_TIME` BIGINT(13) NOT NULL COMMENT '触发时间', `SCHED_TIME` BIGINT(13) NOT NULL COMMENT '下次出发时间', `PRIORITY` INT(11) NOT NULL COMMENT '执行优先级', `STATE` VARCHAR(16) NOT NULL COMMENT '执行权限', `JOB_NAME` VARCHAR(200) DEFAULT NULL COMMENT '调度程序名称', `JOB_GROUP` VARCHAR(200) DEFAULT NULL COMMENT '调度程序名称', `IS_NONCONCURRENT` VARCHAR(1) DEFAULT NULL COMMENT '是否并发', `REQUESTS_RECOVERY` VARCHAR(1) DEFAULT NULL COMMENT '标记是否故障,是否需要回复', PRIMARY KEY (`SCHED_NAME`,`ENTRY_ID`), KEY `IDX_QRTZ_FT_TRIG_INST_NAME` (`SCHED_NAME`,`INSTANCE_NAME`), KEY `IDX_QRTZ_FT_INST_JOB_REQ_RCVRY` (`SCHED_NAME`,`INSTANCE_NAME`,`REQUESTS_RECOVERY`), KEY `IDX_QRTZ_FT_J_G` (`SCHED_NAME`,`JOB_NAME`,`JOB_GROUP`), KEY `IDX_QRTZ_FT_JG` (`SCHED_NAME`,`JOB_GROUP`), KEY `IDX_QRTZ_FT_T_G` (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`), KEY `IDX_QRTZ_FT_TG` (`SCHED_NAME`,`TRIGGER_GROUP`) ) ENGINE=INNODB DEFAULT CHARSET=utf8 # 建表顺序 5 CREATE TABLE `qrtz_job_details` ( `SCHED_NAME` VARCHAR(120) NOT NULL COMMENT '方案名称' , `JOB_NAME` VARCHAR(200) NOT NULL COMMENT '任务名称' , `JOB_GROUP` VARCHAR(200) NOT NULL COMMENT '任务分组' , `DESCRIPTION` VARCHAR(250) DEFAULT NULL COMMENT '任务描述' , `JOB_CLASS_NAME` VARCHAR(250) NOT NULL COMMENT '执行任务的javaclass类' , `IS_DURABLE` VARCHAR(1) NOT NULL COMMENT '是否持久化' , `IS_NONCONCURRENT` VARCHAR(1) NOT NULL COMMENT '是否并发' , `IS_UPDATE_DATA` VARCHAR(1) NOT NULL COMMENT '是否修改数据' , `REQUESTS_RECOVERY` VARCHAR(1) NOT NULL COMMENT '标记是否故障,是否需要回复' , `JOB_DATA` BLOB COMMENT '' , PRIMARY KEY (`SCHED_NAME`,`JOB_NAME`,`JOB_GROUP`), KEY `IDX_QRTZ_J_REQ_RECOVERY` (`SCHED_NAME`,`REQUESTS_RECOVERY`), KEY `IDX_QRTZ_J_GRP` (`SCHED_NAME`,`JOB_GROUP`) ) ENGINE=INNODB DEFAULT CHARSET=utf8 # 建表顺序 6 调度容器 状态信息记录表 CREATE TABLE `qrtz_scheduler_state` ( `SCHED_NAME` VARCHAR(120) NOT NULL COMMENT '方案名称' , `INSTANCE_NAME` VARCHAR(200) NOT NULL COMMENT '实例名称' , `LAST_CHECKIN_TIME` BIGINT(13) NOT NULL COMMENT '最后执行时间' , `CHECKIN_INTERVAL` BIGINT(13) NOT NULL COMMENT '执行检查间隔' , PRIMARY KEY (`SCHED_NAME`,`INSTANCE_NAME`) ) ENGINE=INNODB DEFAULT CHARSET=utf8 # 建表顺序 7 定时器信息记录表 定时器的类型 cron代表 CronTriggerFactoryBean调用 ,simp 代表 SimpleTriggerFactoryBean 调用 CREATE TABLE `qrtz_triggers` ( `SCHED_NAME` VARCHAR(120) NOT NULL COMMENT '方案名称,方案与定时任务是一对多的关系,可以说一个项目一个方法就够,但是一个项目的定时任务是多个', `TRIGGER_NAME` VARCHAR(200) NOT NULL COMMENT '定时任务具体名称', `TRIGGER_GROUP` VARCHAR(200) NOT NULL COMMENT '定时器分组', `JOB_NAME` VARCHAR(200) NOT NULL COMMENT '调度任务名称', `JOB_GROUP` VARCHAR(200) NOT NULL COMMENT '调度任务分组', `DESCRIPTION` VARCHAR(250) DEFAULT NULL COMMENT '定时器的描述', `NEXT_FIRE_TIME` BIGINT(13) DEFAULT NULL COMMENT '下一次执行时间', `PREV_FIRE_TIME` BIGINT(13) DEFAULT NULL COMMENT '上一次执行时间', `PRIORITY` INT(11) DEFAULT NULL COMMENT '定时器的优先执行全选', `TRIGGER_STATE` VARCHAR(16) NOT NULL COMMENT '定时器的执行状态' , `TRIGGER_TYPE` VARCHAR(8) NOT NULL COMMENT '定时器的类型', `START_TIME` BIGINT(13) NOT NULL COMMENT '定时器开始的执行时间', `END_TIME` BIGINT(13) DEFAULT NULL COMMENT '定时器结束执行的时间,(看代码没有找到可配置的地方 )', `CALENDAR_NAME` VARCHAR(200) DEFAULT NULL COMMENT '日历名称(可能是可以指定时区)', `MISFIRE_INSTR` SMALLINT(2) DEFAULT NULL COMMENT '可能是是否启动的标识)', `JOB_DATA` BLOB, PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`), KEY `IDX_QRTZ_T_J` (`SCHED_NAME`,`JOB_NAME`,`JOB_GROUP`), KEY `IDX_QRTZ_T_JG` (`SCHED_NAME`,`JOB_GROUP`), KEY `IDX_QRTZ_T_C` (`SCHED_NAME`,`CALENDAR_NAME`), KEY `IDX_QRTZ_T_G` (`SCHED_NAME`,`TRIGGER_GROUP`), KEY `IDX_QRTZ_T_STATE` (`SCHED_NAME`,`TRIGGER_STATE`), KEY `IDX_QRTZ_T_N_STATE` (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`,`TRIGGER_STATE`), KEY `IDX_QRTZ_T_N_G_STATE` (`SCHED_NAME`,`TRIGGER_GROUP`,`TRIGGER_STATE`), KEY `IDX_QRTZ_T_NEXT_FIRE_TIME` (`SCHED_NAME`,`NEXT_FIRE_TIME`), KEY `IDX_QRTZ_T_NFT_ST` (`SCHED_NAME`,`TRIGGER_STATE`,`NEXT_FIRE_TIME`), KEY `IDX_QRTZ_T_NFT_MISFIRE` (`SCHED_NAME`,`MISFIRE_INSTR`,`NEXT_FIRE_TIME`), KEY `IDX_QRTZ_T_NFT_ST_MISFIRE` (`SCHED_NAME`,`MISFIRE_INSTR`,`NEXT_FIRE_TIME`,`TRIGGER_STATE`), KEY `IDX_QRTZ_T_NFT_ST_MISFIRE_GRP` (`SCHED_NAME`,`MISFIRE_INSTR`,`NEXT_FIRE_TIME`,`TRIGGER_GROUP`,`TRIGGER_STATE`), CONSTRAINT `qrtz_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`) REFERENCES `qrtz_job_details` (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`) ) ENGINE=INNODB DEFAULT CHARSET=utf8 # 建表顺序 8 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, PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`), KEY `SCHED_NAME` (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`), CONSTRAINT `qrtz_blob_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `qrtz_triggers` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) ) ENGINE=INNODB DEFAULT CHARSET=utf8 # 建表顺序 9 存储 Cron Trigger,包括 Cron 表达式和时区信息 CREATE TABLE `qrtz_cron_triggers` ( `SCHED_NAME` VARCHAR(120) NOT NULL COMMENT '方案名称', `TRIGGER_NAME` VARCHAR(200) NOT NULL COMMENT '定时器名称', `TRIGGER_GROUP` VARCHAR(200) NOT NULL COMMENT '定时器分组', `CRON_EXPRESSION` VARCHAR(120) NOT NULL COMMENT '定时器运行时间表达式', `TIME_ZONE_ID` VARCHAR(80) DEFAULT NULL COMMENT ' 运行所用时区', PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`), CONSTRAINT `qrtz_cron_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `qrtz_triggers` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) ) ENGINE=INNODB DEFAULT CHARSET=utf8 # 建表顺序 10 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`), CONSTRAINT `qrtz_simple_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `qrtz_triggers` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) ) ENGINE=INNODB DEFAULT CHARSET=utf8 # 建表顺序 11 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) DEFAULT NULL, `STR_PROP_2` VARCHAR(512) DEFAULT NULL, `STR_PROP_3` VARCHAR(512) DEFAULT NULL, `INT_PROP_1` INT(11) DEFAULT NULL, `INT_PROP_2` INT(11) DEFAULT NULL, `LONG_PROP_1` BIGINT(20) DEFAULT NULL, `LONG_PROP_2` BIGINT(20) DEFAULT NULL, `DEC_PROP_1` DECIMAL(13,4) DEFAULT NULL, `DEC_PROP_2` DECIMAL(13,4) DEFAULT NULL, `BOOL_PROP_1` VARCHAR(1) DEFAULT NULL, `BOOL_PROP_2` VARCHAR(1) DEFAULT NULL, PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`), CONSTRAINT `qrtz_simprop_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `qrtz_triggers` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) ) ENGINE=INNODB DEFAULT CHARSET=utf8 # 建表顺序 12 CREATE TABLE QRTZ_JOB_LISTENERS ( JOB_NAME VARCHAR(200) NOT NULL, JOB_GROUP VARCHAR(200) NOT NULL, JOB_LISTENER VARCHAR(200) NOT NULL, PRIMARY KEY (JOB_NAME,JOB_GROUP,JOB_LISTENER), FOREIGN KEY (JOB_NAME,JOB_GROUP) REFERENCES QRTZ_JOB_DETAILS(JOB_NAME,JOB_GROUP) ) ENGINE=INNODB DEFAULT CHARSET=utf8
# quartz 与Spring集成使用到下面的六个表:其它5张表内无数据,或者其它模式可能用到
qrtz_scheduler_state,
qrtz_triggers,
qrtz_locks,
qrtz_fired_triggers,
qrtz_job_details,
qrtz_cron_triggers
Quartz是OpenSymphony开源组织在任务调度领域的一个开源项目,完全基于Java实现。作为一个优秀的开源调度框架,Quartz具有以下特点:
(1)强大的调度功能,例如支持丰富多样的调度方法,可以满足各种常规及特殊需求;
(2)灵活的应用方式,例如支持任务和调度的多种组合方式,支持调度数据的多种存储方式;
(3)分布式和集群能力,Terracotta收购后在原来功能基础上作了进一步提升。本文将对该部分相加阐述。
Quartz任务调度的核心元素为:Scheduler——任务调度器、Trigger——触发器、Job——任务。其中trigger和job是任务调度的元数据,scheduler是实际执行调度的控制器。
Trigger是用于定义调度时间的元素,即按照什么时间规则去执行任务。Quartz中主要提供了四种类型的trigger:SimpleTrigger,CronTirgger,DateIntervalTrigger,和NthIncludedDayTrigger。这四种trigger可以满足企业应用中的绝大部分需求。
Job用于表示被调度的任务。主要有两种类型的job:无状态的(stateless)和有状态的(stateful)。对于同一个trigger来说,有状态的job不能被并行执行,只有上一次触发的任务被执行完之后,才能触发下一次执行。Job主要有两种属性:volatility和durability,其中volatility表示任务是否被持久化到数据库存储,而durability表示在没有trigger关联的时候任务是否被保留。两者都是在值为true的时候任务被持久化或保留。一个job可以被多个trigger关联,但是一个trigger只能关联一个job。
Scheduler由scheduler工厂创建:DirectSchedulerFactory或者StdSchedulerFactory。第二种工厂StdSchedulerFactory使用较多,因为DirectSchedulerFactory使用起来不够方便,需要作许多详细的手工编码设置。Scheduler主要有三种:RemoteMBeanScheduler,RemoteScheduler和StdScheduler。
Quartz核心元素之间的关系如图1.1所示:
图1.1 核心元素关系图
在Quartz中,有两类线程,Scheduler调度线程和任务执行线程,其中任务执行线程通常使用一个线程池维护一组线程。
图1.2 Quartz线程视图
Scheduler调度线程主要有两个:执行常规调度的线程,和执行misfiredtrigger的线程。常规调度线程轮询存储的所有trigger,如果有需要触发的trigger,即到达了下一次触发的时间,则从任务执行线程池获取一个空闲线程,执行与该trigger关联的任务。Misfire线程是扫描所有的trigger,查看是否有misfiredtrigger,如果有的话根据misfire的策略分别处理(fire now OR wait for the next fire)。
Quartz中的trigger和job需要存储下来才能被使用。Quartz中有两种存储方式:RAMJobStore,JobStoreSupport,其中RAMJobStore是将trigger和job存储在内存中,而JobStoreSupport是基于jdbc将trigger和job存储到数据库中。RAMJobStore的存取速度非常快,但是由于其在系统被停止后所有的数据都会丢失,所以在集群应用中,必须使用JobStoreSupport。
一个Quartz集群中的每个节点是一个独立的Quartz应用,它又管理着其他的节点。这就意味着你必须对每个节点分别启动或停止。Quartz集群中,独立的Quartz节点并不与另一其的节点或是管理节点通信,而是通过相同的数据库表来感知到另一Quartz应用的,如图2.1所示。
图2.1 Quartz集群架构
因为Quartz集群依赖于数据库,所以必须首先创建Quartz数据库表,Quartz发布包中包括了所有被支持的数据库平台的SQL脚本。这些SQL脚本存放于<quartz_home>/docs/dbTables 目录下。这里采用的Quartz 1.8.4版本,总共12张表,不同版本,表个数可能不同。数据库为mysql,用tables_mysql.sql创建数据库表。全部表如图2.2所示,对这些表的简要介绍如图2.3所示。
图2.2 Quartz 1.8.4在mysql数据库中生成的表
图2.3 Quartz数据表简介
说明:集群中节点实例信息,Quartz定时读取该表的信息判断集群中每个实例的当前状态。
instance_name:配置文件中org.quartz.scheduler.instanceId配置的名字,如果设置为AUTO,quartz会根据物理机名和当前时间产生一个名字。
last_checkin_time:上次检入时间
checkin_interval:检入间隔时间
存储与已触发的Trigger相关的状态信息,以及相联Job的执行信息。
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表达式
说明:保存job详细信息,该表需要用户根据实际情况初始化
job_name:集群中job的名字,该名字用户自己可以随意定制,无强行要求。
job_group:集群中job的所属组的名字,该名字用户自己随意定制,无强行要求。
job_class_name:集群中job实现类的完全包名,quartz就是根据这个路径到classpath找到该job类的。
is_durable:是否持久化,把该属性设置为1,quartz会把job持久化到数据库中
job_data:一个blob字段,存放持久化job对象。
说明:tables_oracle.sql里有相应的dml初始化,如图2.4所示。
图2.4 Quartz权限信息表中的初始化信息
Quartz Scheduler自身是察觉不到被集群的,只有配置给Scheduler的JDBC JobStore才知道。当Quartz Scheduler启动时,它调用JobStore的schedulerStarted()方法,它告诉JobStore Scheduler已经启动了。schedulerStarted() 方法是在JobStoreSupport类中实现的。JobStoreSupport类会根据quartz.properties文件中的设置来确定Scheduler实例是否参与到集群中。假如配置了集群,一个新的ClusterManager类的实例就被创建、初始化并启动。ClusterManager是在JobStoreSupport类中的一个内嵌类,继承了java.lang.Thread,它会定期运行,并对Scheduler实例执行检入的功能。Scheduler也要查看是否有任何一个别的集群节点失败了。检入操作执行周期在quartz.properties中配置。
当一个Scheduler实例执行检入时,它会查看是否有其他的Scheduler实例在到达他们所预期的时间还未检入。这是通过检查SCHEDULER_STATE表中Scheduler记录在LAST_CHEDK_TIME列的值是否早于org.quartz.jobStore.clusterCheckinInterval来确定的。如果一个或多个节点到了预定时间还没有检入,那么运行中的Scheduler就假定它(们) 失败了。
当一个Sheduler实例在执行某个Job时失败了,有可能由另一正常工作的Scheduler实例接过这个Job重新运行。要实现这种行为,配置给JobDetail对象的Job可恢复属性必须设置为true(job.setRequestsRecovery(true))。如果可恢复属性被设置为false(默认为false),当某个Scheduler在运行该job失败时,它将不会重新运行;而是由另一个Scheduler实例在下一次触发时间触发。Scheduler实例出现故障后多快能被侦测到取决于每个Scheduler的检入间隔(即2.3中提到的org.quartz.jobStore.clusterCheckinInterval)。
Spring从2.0.2开始便不再支持Quartz。具体表现在 Quartz+Spring 把 Quartz 的 Task 实例化进入数据库时,会产生: Serializable 的错误:
<bean id="jobtask" class="org.springframework.scheduling.quartz. MethodInvokingJobDetailFactoryBean "> <property name="targetObject"> <ref bean="quartzJob"/> </property> <property name="targetMethod"> <value>execute</value> </property> </bean>这个 MethodInvokingJobDetailFactoryBean 类中的 methodInvoking 方法,是不支持序列化的,因此在把 QUARTZ 的 TASK 序列化进入数据库时就会抛错。
首先解决MethodInvokingJobDetailFactoryBean的问题,在不修改Spring源码的情况下,可以避免使用这个类,直接调用JobDetail。但是使用JobDetail实现,需要自己实现MothodInvoking的逻辑,可以使用JobDetail的jobClass和JobDataAsMap属性来自定义一个Factory(Manager)来实现同样的目的。例如,本示例中新建了一个MyDetailQuartzJobBean来实现这个功能。
在Test类中,只是简单实现了打印系统当前时间的功能。
package org.lxh.mvc.job; import java.io.Serializable; import java.util.Date; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; public class Test implements Serializable{ private Log logger = LogFactory.getLog(Test.class); private static final long serialVersionUID = -2073310586499744415L; public void execute () { Date date=new Date(); System.out.println(date.toLocaleString()); } }ServerA、ServerB的代码、配置完全一样,先启动ServerA,后启动ServerB,当Server关断之后,ServerB会监测到其关闭,并将ServerA上正在执行的Job接管,继续执行。
尽管我们已经实现了Spring+Quartz的集群配置,但是因为Spring与Quartz之间的兼容问题还是不建议使用该方式。在本小节中,我们实现了单独用Quartz配置的集群,相对Spring+Quartz的方式来说,简单、稳定。
我们采用单独使用Quartz来实现其集群功能,代码结构及所需的第三方jar包如图3.1所示。其中,Mysql版本:5.1.52,Mysql驱动版本:mysql-connector-java-5.1.5-bin.jar(针对于5.1.52,建议采用该版本驱动,因为Quartz存在BUG使得其与某些Mysql驱动结合时不能正常运行)。
图4.1 Quartz集群工程结构及所需第三方jar包
其中quartz.properties为Quartz配置文件,放在src目录下,若无该文件,Quartz将自动加载jar包中的quartz.properties文件;SimpleRecoveryJob.java、SimpleRecoveryStatefulJob.java为两个Job;ClusterExample.java中编写了调度信息、触发机制及相应的测试main函数。
默认文件名称quartz.properties,通过设置"org.quartz.jobStore.isClustered"属性为"true"来激活集群特性。在集群中的每一个实例都必须有一个唯一的"instance id" ("org.quartz.scheduler.instanceId" 属性), 但是应该有相同的"scheduler instance name" ("org.quartz.scheduler.instanceName"),也就是说集群中的每一个实例都必须使用相同的quartz.properties 配置文件。除了以下几种例外,配置文件的内容其他都必须相同:
a.线程池大小。
b.不同的"org.quartz.scheduler.instanceId"属性值(通过设定为"AUTO"即可)。
#============================================================== #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 = trueServer A与Server B中的配置和代码完全一样。运行方法:运行任意主机上的ClusterExample.java,将任务加入调度,观察运行结果:
运行ServerA,结果如图4.2所示。
图4.2 ServerA运行结果1
开启ServerB后,ServerA与ServerB的输出如图4.3、4.4所示。
图4.3 ServerA运行结果2
图4.4 ServerB运行结果1
从图4.3、4.4可以看出,ServerB开启后,系统自动实现了负责均衡,ServerB接手Job1。关断ServerA后,ServerB的运行结果如图4.5所示。
图4.5 ServerB运行结果2
从图4.5中可以看出,ServerB可以检测出ServerA丢失,将其负责的任务Job2接手,并将ServerA丢失到Server检测出这段异常时间中需要执行的Job2重新执行了。
Quartz实际并不关心你是在相同还是不同的机器上运行节点。当集群放置在不同的机器上时,称之为水平集群。节点跑在同一台机器上时,称之为垂直集群。对于垂直集群,存在着单点故障的问题。这对高可用性的应用来说是无法接受的,因为一旦机器崩溃了,所有的节点也就被终止了。对于水平集群,存在着时间同步问题。
节点用时间戳来通知其他实例它自己的最后检入时间。假如节点的时钟被设置为将来的时间,那么运行中的Scheduler将再也意识不到那个结点已经宕掉了。另一方面,如果某个节点的时钟被设置为过去的时间,也许另一节点就会认定那个节点已宕掉并试图接过它的Job重运行。最简单的同步计算机时钟的方式是使用某一个Internet时间服务器(Internet Time Server ITS)。
因为Quartz使用了一个随机的负载均衡算法, Job以随机的方式由不同的实例执行。Quartz官网上提到当前,还不存在一个方法来指派(钉住) 一个 Job 到集群中特定的节点。
当前,如果不直接进到数据库查询的话,还没有一个简单的方式来得到集群中所有正在执行的Job列表。请求一个Scheduler实例,将只能得到在那个实例上正运行Job的列表。Quartz官网建议可以通过写一些访问数据库JDBC代码来从相应的表中获取全部的Job信息。
quartz 执行的sql 顺序:
1. select TRIGGER_ACCESS from QRTZ2_LOCKS for update 2. SELECT TRIGGER_NAME, TRIGGER_GROUP, NEXT_FIRE_TIME, PRIORITY FROM QRTZ2_TRIGGERS WHERE SCHEDULER_NAME = 'CRMscheduler' AND TRIGGER_STATE = 'ACQUIRED' AND NEXT_FIRE_TIME <= '{timekey 30s latter}' AND ( MISFIRE_INSTR = -1 OR ( MISFIRE_INSTR != -1 AND NEXT_FIRE_TIME >= '{timekey now}' ) ) ORDER BY NEXT_FIRE_TIME ASC, PRIORITY DESC; 3. SELECT * FROM QRTZ2_JOB_DETAILS WHERE SCHEDULER_NAME = CRMscheduler AND JOB_NAME = ? AND JOB_GROUP = ?; 4. UPDATE TQRTZ2_TRIGGERS SET TRIGGER_STATE = 'ACQUIRED' WHERE SCHED_NAME = 'CRMscheduler' AND TRIGGER_NAME = '{triggerName}' AND TRIGGER_GROUP = '{triggerGroup}' AND TRIGGER_STATE = 'waiting'; 5. INSERT INTO QRTZ2_FIRED_TRIGGERS (SCHEDULER_NAME, ENTRY_ID, TRIGGER_NAME, TRIGGER_GROUP, INSTANCE_NAME, FIRED_TIME, SCHED_TIME, STATE, JOB_NAME, JOB_GROUP, IS_NONCONCURRENT, REQUESTS_RECOVERY, PRIORITY) VALUES( 'CRMscheduler', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); 6. commit; 7. select STAT_ACCESS from QRTZ2_LOCKS for update 8. SELECT TRIGGER_STATE FROM QRTZ2_TRIGGERS WHERE SCHEDULER_NAME = 'CRMscheduler' AND TRIGGER_NAME = ? AND TRIGGER_GROUP = ?; 9. SELECT TRIGGER_STATE FROM QRTZ2_TRIGGERS WHERE SCHEDULER_NAME = 'CRMscheduler' AND TRIGGER_NAME = ? AND TRIGGER_GROUP = ?; 10. SELECT * FROM QRTZ2_JOB_DETAILS WHERE SCHEDULER_NAME = CRMscheduler AND JOB_NAME = ? AND JOB_GROUP = ?; 11. SELECT * FROM QRTZ2_CALENDARS WHERE SCHEDULER_NAME = 'CRMscheduler' AND CALENDAR_NAME = ?; 12. UPDATE QRTZ2_FIRED_TRIGGERS SET INSTANCE_NAME = ?, FIRED_TIME = ?, SCHED_TIME = ?, ENTRY_STATE = ?, JOB_NAME = ?, JOB_GROUP = ?, IS_NONCONCURRENT = ?, REQUESTS_RECOVERY = ? WHERE SCHEDULER_NAME = 'CRMscheduler' AND ENTRY_ID = ?; 13. UPDATE TQRTZ2_TRIGGERS SET TRIGGER_STATE = ? WHERE SCHED_NAME = 'CRMscheduler' AND TRIGGER_NAME = '{triggerName}' AND TRIGGER_GROUP = '{triggerGroup}' AND TRIGGER_STATE = ?; 14. UPDATE QRTZ2_TRIGGERS SET JOB_NAME = ?, JOB_GROUP = ?, DESCRIPTION = ?, NEXT_FIRE_TIME = ?, PREV_FIRE_TIME = ?, TRIGGER_STATE = ?, TRIGGER_TYPE = ?, START_TIME = ?, END_TIME = ?, CALENDAR_NAME = ?, MISFIRE_INSTRUCTION = ?, PRIORITY = ?, JOB_DATAMAP = ? WHERE SCHEDULER_NAME = SCHED_NAME_SUBST AND TRIGGER_NAME = ? AND TRIGGER_GROUP = ?; 15. commit;