在半個月之前,有幸看了xxl-job源碼,原本打算寫一篇源碼分析文章。結果由于瑣碎的事情干擾了,擱淺了。本篇文章先預熱一下,講下xxl-job中關于quartz知識。(本文內容參考自xxl-job官網)
xxl-job設計思想和調度模塊剖析
xxl-job將調度行為抽象形成"調度中心"公共平臺,而平臺自身并不承擔業務邏輯,"調度中心"負責發起調度請求。將任務抽象成分散的JobHandler,交由"執行器"統一管理,"執行器"負責接收調度請求并執行對應的JobHandler中業務邏輯。因此,"調度"和"任務"兩部分可以相互解耦,提高系統整體穩定性和擴展性。
quartz的不足
Quartz作為開源作業調度中的佼佼者,是作業調度的首選。集群環境中Quartz采用API的方式對任務進行管理,但是會存在以下問題:
- 調用API的方式操作任務,不人性化。
- 需要持久化業務QuartzJobBean到底層數據表中,系統侵入性相當嚴重。
- 調度邏輯和QuartzJobBean耦合在同一個項目中,這將導致一個問題,在調度任務數量逐漸增多,同時調度任務邏輯逐漸加重的情況下,此時調度系統的性能將大大受限于業務。
- Quartz底層以"搶占式"獲取DB鎖并由搶占成功節點負責運行任務,會導致節點負載懸殊非常大,而xxl-job通過執行器實現"協同分配式"運行任務,充分發揮集群優勢,負載各節點均衡。
RemoteHttpJobBean
常規Quartz的開發,任務邏輯一般維護在QuartzJobBean中,耦合很嚴重。xxl-job中的調度模塊和任務模塊完全解耦,調度模塊中的所有調度任務都使用的是同一個QuartzJobBean(也就是RemoteHttpJobBean)。不同的調度任務將各自參數維護在各自擴展表數據中,當觸發RemoteHttpJobBean執行時,將會解析不同的任務參數發起遠程調用,調用各自的遠程執行器服務。這種調用模型類似RPC調用,RemoteHttpJobBean提供調用代理的功能,而執行器提供遠程服務的功能。
調度中心HA(集群)
基于Quartz的集群方案,數據庫選用Mysql;集群分布式并發環境中使用QUARTZ定時任務調度,會在各個節點會上報任務,存到數據庫中,執行時會從數據庫中取出觸發器來執行,如果觸發器的名稱和執行時間相同,則只有一個節點去執行此任務。
# 基于Quartz的集群方案,數據庫選用Mysql;
# 集群分布式并發環境中使用QUARTZ定時任務調度,會在各個節點會上報任務,存到數據庫中。
# 執行時會從數據庫中取出觸發器來執行,如果觸發器的名稱和執行時間相同,則只有一個節點去執行此任務。
# for cluster
org.quartz.jobStore.tablePrefix: XXL_JOB_QRTZ_
org.quartz.jobStore.class: org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.isClustered: true
org.quartz.jobStore.clusterCheckinInterval: 5000
調度線程池
調度采用線程池方式實現,避免單線程因阻塞而引起任務調度延遲
# 調度采用線程池方式實現,避免單線程因阻塞而引起任務調度延遲。
org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount: 50
org.quartz.threadPool.threadPriority: 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true
@DisallowConcurrentExecution
xxl-job調度模塊的"調度中心"默認不使用該注解,因為RemoteHttpJobBean為公共QuartzJobBean,這樣在多線程調度的情況下,調度模塊被阻塞的幾率很低,大大提高了調度系統的承載量。xxl-job的每個調度任務雖然在調度模塊時并行調度執行的,但是任務調度傳遞到任務模塊的執行器確實是串行執行的,同時支持任務終止。
話外音:Quartz定時任務默認都是并發執行的,不會等待上一次任務執行完畢,只要間隔時間到就會執行, 如果定時任執行太長,會長時間占用資源,導致其它任務堵塞。
misfire策略
misfire用于Trigger觸發時,線程池中沒有可用的線程或者調度器被關閉了,此時這個Trigger就變成了misfire。當下次調度器啟動或者有線程可用時,會檢查處于misfire狀態的Trigger。而misfire的狀態值決定了調度器如何處理這個Trigger。
quartz有個全局的參數misfireThreadshold可以設置允許的超時時間,單位為毫秒。如果超過了就不執行,未超過就執行。
org.quartz.jobStore.misfireThreshold: 60000
造成misfire的可能原因:服務重啟;調度線程被QuartzJobBean阻塞,線程被耗盡;某個任務啟用了@DisallowConcurrentExecution,上次調度持續阻塞,下次調度被錯過。
Misfire規則,假設任務是從上午9點到下午17點時間范圍執行:
withMisfireHandlingInstructionDoNothing:不觸發立即執行,等待下次調度
withMisfireHandlingInstructionIgnoreMisfires:以錯過的第一個頻率時間立刻開始執行,重做錯過的所有頻率周期后,當下一次觸發頻率發生時間大于當前時間后,再按照正常的Cron頻率依次執行。如果9點misfire了,在10:15系統恢復之后。9點,10點的misfire會馬上執行。
withMisfireHandlingInstructionFireAndProceed:以當前時間為觸發頻率立刻觸發一次執行,然后按照Cron頻率依次執行。假設9點,10點的任務都misfire了,系統在10:15恢復后,只會執行一次misfire,下次正點執行。
xxl-job默認misfire規則為:withMisfireHandlingInstructionDoNothing
// 3、corn trigger
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression).withMisfireHandlingInstructionDoNothing(); // withMisfireHandlingInstructionDoNothing 忽略掉調度終止過程中忽略的調度
CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity(triggerKey).withSchedule(cronScheduleBuilder).build();
xxl-job和quartz數據庫表講解
XXL-JOB調度模塊基于Quartz集群實現,其"調度數據庫"是在Quartz的11張集群mysql表基礎上擴展而成。
xxl-job的5張擴展表
XXL_JOB_QRTZ_TRIGGER_GROUP
:執行器信息表,維護任務執行器信息
CREATE TABLE `XXL_JOB_QRTZ_TRIGGER_GROUP` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`app_name` varchar(64) NOT NULL COMMENT '執行器AppName',
`title` varchar(12) NOT NULL COMMENT '執行器名稱',
`order` tinyint(4) NOT NULL DEFAULT '0' COMMENT '排序',
`address_type` tinyint(4) NOT NULL DEFAULT '0' COMMENT '執行器地址類型:0=自動注冊、1=手動錄入',
`address_list` varchar(512) DEFAULT NULL COMMENT '執行器地址列表,多地址逗號分隔',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
XXL_JOB_QRTZ_TRIGGER_INFO
:調度擴展信息表, 用于保存XXL-JOB調度任務的擴展信息,如任務分組、任務名、機器地址、執行器、執行入參和報警郵件等等
CREATE TABLE `XXL_JOB_QRTZ_TRIGGER_INFO` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`job_group` int(11) NOT NULL COMMENT '執行器主鍵ID',
`job_cron` varchar(128) NOT NULL COMMENT '任務執行CRON',
`job_desc` varchar(255) NOT NULL,
`add_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`author` varchar(64) DEFAULT NULL COMMENT '作者',
`alarm_email` varchar(255) DEFAULT NULL COMMENT '報警郵件',
`executor_route_strategy` varchar(50) DEFAULT NULL COMMENT '執行器路由策略',
`executor_handler` varchar(255) DEFAULT NULL COMMENT '執行器任務handler',
`executor_param` varchar(512) DEFAULT NULL COMMENT '執行器任務參數',
`executor_block_strategy` varchar(50) DEFAULT NULL COMMENT '阻塞處理策略',
`executor_timeout` int(11) NOT NULL DEFAULT '0' COMMENT '任務執行超時時間,單位秒',
`executor_fail_retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '失敗重試次數',
`glue_type` varchar(50) NOT NULL COMMENT 'GLUE類型',
`glue_source` mediumtext COMMENT 'GLUE源代碼',
`glue_remark` varchar(128) DEFAULT NULL COMMENT 'GLUE備注',
`glue_updatetime` datetime DEFAULT NULL COMMENT 'GLUE更新時間',
`child_jobid` varchar(255) DEFAULT NULL COMMENT '子任務ID,多個逗號分隔',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
XXL_JOB_QRTZ_TRIGGER_LOG
:調度日志表,用于保存XXL-JOB任務調度的歷史信息,如調度結果、執行結果、調度入參、調度機器和執行器等等;
CREATE TABLE `XXL_JOB_QRTZ_TRIGGER_LOG` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`job_group` int(11) NOT NULL COMMENT '執行器主鍵ID',
`job_id` int(11) NOT NULL COMMENT '任務,主鍵ID',
`executor_address` varchar(255) DEFAULT NULL COMMENT '執行器地址,本次執行的地址',
`executor_handler` varchar(255) DEFAULT NULL COMMENT '執行器任務handler',
`executor_param` varchar(512) DEFAULT NULL COMMENT '執行器任務參數',
`executor_sharding_param` varchar(20) DEFAULT NULL COMMENT '執行器任務分片參數,格式如 1/2',
`executor_fail_retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '失敗重試次數',
`trigger_time` datetime DEFAULT NULL COMMENT '調度-時間',
`trigger_code` int(11) NOT NULL COMMENT '調度-結果',
`trigger_msg` text COMMENT '調度-日志',
`handle_time` datetime DEFAULT NULL COMMENT '執行-時間',
`handle_code` int(11) NOT NULL COMMENT '執行-狀態',
`handle_msg` text COMMENT '執行-日志',
PRIMARY KEY (`id`),
KEY `I_trigger_time` (`trigger_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
XXL_JOB_QRTZ_TRIGGER_LOGGLUE
:任務GLUE日志,用于保存GLUE更新歷史,用于支持GLUE的版本回溯功能;
CREATE TABLE `XXL_JOB_QRTZ_TRIGGER_LOGGLUE` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`job_id` int(11) NOT NULL COMMENT '任務,主鍵ID',
`glue_type` varchar(50) DEFAULT NULL COMMENT 'GLUE類型',
`glue_source` mediumtext COMMENT 'GLUE源代碼',
`glue_remark` varchar(128) NOT NULL COMMENT 'GLUE備注',
`add_time` timestamp NULL DEFAULT NULL,
`update_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
XXL_JOB_QRTZ_TRIGGER_REGISTRY
:執行器注冊表,維護在線的執行器和調度中心機器地址信息;
CREATE TABLE XXL_JOB_QRTZ_TRIGGER_REGISTRY (
`id` int(11) NOT NULL AUTO_INCREMENT,
`registry_group` varchar(255) NOT NULL,
`registry_key` varchar(255) NOT NULL,
`registry_value` varchar(255) NOT NULL,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
quartz的11張系統表(只會介紹常見的,后續用到了其他的再補充)
XXL_JOB_QRTZ_JOB_DETAILS
:存儲的是job的詳細信息,包括:[DESCRIPTION]
描述,[IS_DURABLE]
是否持久化,[JOB_DATA]
持久化對象等基本信息。
CREATE TABLE XXL_JOB_QRTZ_JOB_DETAILS
(
SCHED_NAME VARCHAR(120) NOT NULL,
JOB_NAME VARCHAR(200) NOT NULL,
JOB_GROUP VARCHAR(200) NOT NULL,
DESCRIPTION VARCHAR(250) NULL,
JOB_CLASS_NAME VARCHAR(250) NOT NULL,
IS_DURABLE VARCHAR(1) NOT NULL,
IS_NONCONCURRENT VARCHAR(1) NOT NULL,
IS_UPDATE_DATA VARCHAR(1) NOT NULL,
REQUESTS_RECOVERY VARCHAR(1) NOT NULL,
JOB_DATA BLOB NULL,
PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)
);
XXL_JOB_QRTZ_CRON_TRIGGERS
:存儲CronTrigger
相關信息,這也是我們使用最多的觸發器。
CREATE TABLE XXL_JOB_QRTZ_CRON_TRIGGERS
(
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
CRON_EXPRESSION VARCHAR(200) NOT NULL,
TIME_ZONE_ID VARCHAR(80),
PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES XXL_JOB_QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);
XXL_JOB_QRTZ_PAUSED_TRIGGER_GRPS
:存放暫停掉的觸發器
CREATE TABLE XXL_JOB_QRTZ_PAUSED_TRIGGER_GRPS
(
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP)
);
XXL_JOB_QRTZ_FIRED_TRIGGERS
:存儲已經觸發的trigger相關信息,trigger隨著時間的推移狀態發生變化,直到最后trigger執行完成,從表中被刪除。相同的trigger和task,每觸發一次都會創建一個實例;從剛被創建的ACQUIRED狀態,到EXECUTING狀態,最后執行完從數據庫中刪除;
CREATE TABLE XXL_JOB_QRTZ_FIRED_TRIGGERS
(
SCHED_NAME VARCHAR(120) NOT NULL,
ENTRY_ID VARCHAR(95) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
INSTANCE_NAME VARCHAR(200) NOT NULL,
FIRED_TIME BIGINT(13) NOT NULL,
SCHED_TIME BIGINT(13) NOT NULL,
PRIORITY INTEGER NOT NULL,
STATE VARCHAR(16) NOT NULL,
JOB_NAME VARCHAR(200) NULL,
JOB_GROUP VARCHAR(200) NULL,
IS_NONCONCURRENT VARCHAR(1) NULL,
REQUESTS_RECOVERY VARCHAR(1) NULL,
PRIMARY KEY (SCHED_NAME,ENTRY_ID)
);
XXL_JOB_QRTZ_SIMPLE_TRIGGERS
:存儲SimpleTrigger信息,timesTriggered默認值為0,當timesTriggered > repeatCount停止trigger,當執行完畢之后此記錄會被刪除
CREATE TABLE XXL_JOB_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),
FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES XXL_JOB_QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);
XXL_JOB_QRTZ_TRIGGERS
: 和XXL_JOB_QRTZ_FIRED_TRIGGERS
存放的不一樣,不管trigger
觸發了多少次都只有一條記錄,TRIGGER_STATE
用來標識當前trigger
的狀態;假如cronTrigger每小時執行一次,執行完之后一直是WAITING狀態;假如cronTrigger每6秒執行一次狀態是ACQUIRED狀態;假如simpleTrigger設置的執行次數為5,那么重復執行5次后狀態為COMPLETE,并且會被刪除。
CREATE TABLE XXL_JOB_QRTZ_TRIGGERS
(
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
JOB_NAME VARCHAR(200) NOT NULL,
JOB_GROUP VARCHAR(200) NOT NULL,
DESCRIPTION VARCHAR(250) NULL,
NEXT_FIRE_TIME BIGINT(13) NULL,
PREV_FIRE_TIME BIGINT(13) NULL,
PRIORITY INTEGER NULL,
TRIGGER_STATE VARCHAR(16) NOT NULL,
TRIGGER_TYPE VARCHAR(8) NOT NULL,
START_TIME BIGINT(13) NOT NULL,
END_TIME BIGINT(13) NULL,
CALENDAR_NAME VARCHAR(200) NULL,
MISFIRE_INSTR SMALLINT(2) NULL,
JOB_DATA BLOB NULL,
PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)
REFERENCES XXL_JOB_QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP)
);
XXL_JOB_QRTZ_SCHEDULER_STATE
:存儲所有節點的scheduler
,會定期檢查scheduler
是否失效。
CREATE TABLE XXL_JOB_QRTZ_SCHEDULER_STATE
(
SCHED_NAME VARCHAR(120) NOT NULL,
INSTANCE_NAME VARCHAR(200) NOT NULL,
LAST_CHECKIN_TIME BIGINT(13) NOT NULL,
CHECKIN_INTERVAL BIGINT(13) NOT NULL,
PRIMARY KEY (SCHED_NAME,INSTANCE_NAME)
);
XXL_JOB_QRTZ_LOCKS
:Quartz提供的鎖表,為多個節點調度提供分布式鎖,實現分布式調度,默認有2個鎖,STATE_ACCESS
主要用在scheduler定期檢查是否失效的時候,保證只有一個節點去處理已經失效的scheduler
。TRIGGER_ACCESS
主要用在TRIGGER
被調度的時候,保證只有一個節點去執行調度。
CREATE TABLE XXL_JOB_QRTZ_LOCKS
(
SCHED_NAME VARCHAR(120) NOT NULL,
LOCK_NAME VARCHAR(40) NOT NULL,
PRIMARY KEY (SCHED_NAME,LOCK_NAME)
);
關于單機版的quartz與Spring XML集成
1.創建Job類,無須繼承父類,直接配置MethodInvokingJobDetailFactoryBean
即可。但需要指定一下兩個屬性:
- targetObject:指定包含任務執行體的Bean實例。
- targetMethod:指定將指定Bean實例的該方法包裝成任務的執行體
<!-- 配置Job類 -->
<bean id="myJob" class="com.cmazxiaoma.quartz.xml.MyJob"></bean>
<!-- 配置JobDetail -->
<bean id="myJobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<!-- 執行目標job -->
<property name="targetObject" ref="myJob"></property>
<!-- 要執行的方法 -->
<property name="targetMethod" value="execute"></property>
</bean>
<!-- 配置tirgger觸發器 -->
<bean id="cronTriggerFactoryBean" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<!-- jobDetail -->
<property name="jobDetail" ref="myJobDetail"></property>
<!-- cron表達式,執行時間 每5秒執行一次 -->
<property name="cronExpression" value="0/5 * * * * ?"></property>
</bean>
<!-- 配置調度工廠 -->
<bean id="springJobSchedulerFactoryBean" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<ref bean="cronTriggerFactoryBean"></ref>
</list>
</property>
</bean>
根據jdi.getJobDataMap().put("methodInvoker", this);
來看,如果Quartz是集群的話,這里會拋出Couldn't store job: Unable to serialize JobDataMap for insertion into database because the value of property 'methodInvoker' is not serializable: org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean
因為methodInvoker
不能進行序列化,沒有實現serializable
接口。集群環境下,還是推薦用繼承QuartzJobBean創建Job。MethodInvokingJobDetailFactoryBean中的targetObject和targetMethod參數無法持久化到底層數據庫表。
最終還是根據concurrent策略選擇MethodInvokingJob或者StatefulMethodInvokingJob去反射調用targetObject中的targetMethod。
public static class MethodInvokingJob extends QuartzJobBean {
protected static final Log logger = LogFactory.getLog(MethodInvokingJob.class);
private MethodInvoker methodInvoker;
/**
* Set the MethodInvoker to use.
*/
public void setMethodInvoker(MethodInvoker methodInvoker) {
this.methodInvoker = methodInvoker;
}
/**
* Invoke the method via the MethodInvoker.
*/
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
try {
context.setResult(this.methodInvoker.invoke());
}
catch (InvocationTargetException ex) {
if (ex.getTargetException() instanceof JobExecutionException) {
// -> JobExecutionException, to be logged at info level by Quartz
throw (JobExecutionException) ex.getTargetException();
}
else {
// -> "unhandled exception", to be logged at error level by Quartz
throw new JobMethodInvocationFailedException(this.methodInvoker, ex.getTargetException());
}
}
catch (Exception ex) {
// -> "unhandled exception", to be logged at error level by Quartz
throw new JobMethodInvocationFailedException(this.methodInvoker, ex);
}
}
}
/**
* Extension of the MethodInvokingJob, implementing the StatefulJob interface.
* Quartz checks whether or not jobs are stateful and if so,
* won't let jobs interfere with each other.
*/
@PersistJobDataAfterExecution
@DisallowConcurrentExecution
public static class StatefulMethodInvokingJob extends MethodInvokingJob {
// No implementation, just an addition of the tag interface StatefulJob
// in order to allow stateful method invoking jobs.
}
2.創建Job
類,myJob
類繼承QuartzJobBean
,實現 executeInternal(JobExecutionContext jobexecutioncontext)
方法。然后再通過配置JobDetailFactoryBean
創建jobDetail
。
<!-- 配置JobDetail -->
<bean id="myJobDetail" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
<property name="jobClass" value="com.cmazxiaoma.quartz.xml.MyJob"></property>
<property name="durability" value="true"></property>
</bean>
<!-- 配置tirgger觸發器 -->
<bean id="cronTriggerFactoryBean" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<!-- jobDetail -->
<property name="jobDetail" ref="myJobDetail"></property>
<!-- cron表達式,執行時間 每5秒執行一次 -->
<property name="cronExpression" value="0/5 * * * * ?"></property>
</bean>
<!-- 配置調度工廠 如果將lazy-init='false'那么容器啟動就會執行調度程序 -->
<bean id="springJobSchedulerFactoryBean" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<ref bean="cronTriggerFactoryBean"></ref>
</list>
</property>
</bean>
requestsRecovery屬性設置為 true時,當Quartz服務被中止后,再次啟動或集群中其他機器接手任務時會嘗試恢復執行之前未完成的所有任務。
集群Quartz與SpringBoot集成
@Configuration
public class XxlJobDynamicSchedulerConfig {
@Bean
public SchedulerFactoryBean getSchedulerFactoryBean(DataSource dataSource){
SchedulerFactoryBean schedulerFactory = new SchedulerFactoryBean();
schedulerFactory.setDataSource(dataSource);
// 自動啟動
schedulerFactory.setAutoStartup(true);
// 延時啟動,應用啟動成功后在啟動
schedulerFactory.setStartupDelay(20);
// 覆蓋DB中JOB:true、以數據庫中已經存在的為準:false
schedulerFactory.setOverwriteExistingJobs(true);
schedulerFactory.setApplicationContextSchedulerContextKey("applicationContext");
schedulerFactory.setConfigLocation(new ClassPathResource("quartz.properties"));
return schedulerFactory;
}
@Bean(initMethod = "start", destroyMethod = "destroy")
public XxlJobDynamicScheduler getXxlJobDynamicScheduler(SchedulerFactoryBean schedulerFactory){
Scheduler scheduler = schedulerFactory.getScheduler();
XxlJobDynamicScheduler xxlJobDynamicScheduler = new XxlJobDynamicScheduler();
xxlJobDynamicScheduler.setScheduler(scheduler);
return xxlJobDynamicScheduler;
}
}
quartz.properties
# Default Properties file for use by StdSchedulerFactory
# to create a Quartz Scheduler Instance, if a different
# properties file is not explicitly specified.
#
org.quartz.scheduler.instanceName: DefaultQuartzScheduler
org.quartz.scheduler.instanceId: AUTO
org.quartz.scheduler.rmi.export: false
org.quartz.scheduler.rmi.proxy: false
org.quartz.scheduler.wrapJobExecutionInUserTransaction: false
# 調度采用線程池方式實現,避免單線程因阻塞而引起任務調度延遲。
org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount: 50
org.quartz.threadPool.threadPriority: 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true
# misfile:錯過了觸發時間,來處理規則。
# 可能原因:
# 1.服務重啟 2.調度線程被QuartzJobBean阻塞 3.線程被耗盡
# 4.某個任務啟動了@DisallowConcurrentExecution,上次調度持續阻塞,下次調度被錯過。
# 假設任務是從上午9點到下午17點
# Misfire規則:
# withMisfireHandlingInstructionDoNothing:不觸發立即執行,等待下次調度
#——以錯過的第一個頻率時間立刻開始執行
#——重做錯過的所有頻率周期后
#——當下一次觸發頻率發生時間大于當前時間后,再按照正常的Cron頻率依次執行
# 如果9點misfire了,在10:15系統恢復之后。9點,10點的misfire會馬上執行
# withMisfireHandlingInstructionIgnoreMisfires:以錯過的第一個頻率時間立刻開始執行;
#——以當前時間為觸發頻率立刻觸發一次執行
# ——然后按照Cron頻率依次執行
# withMisfireHandlingInstructionFireAndProceed:以當前時間為觸發頻率立刻觸發一次執行;
# 假設9點,10點的任務都misfire了,系統在10:15恢復后,只會執行一次misfire,
# 下次正點執行。
# XXL-JOB默認misfire規則為:withMisfireHandlingInstructionDoNothing
### 單位毫秒
org.quartz.jobStore.misfireThreshold: 60000
org.quartz.jobStore.maxMisfiresToHandleAtATime: 1
#org.quartz.jobStore.class: org.quartz.simpl.RAMJobStore
# 基于Quartz的集群方案,數據庫選用Mysql;
# 集群分布式并發環境中使用QUARTZ定時任務調度,會在各個節點會上報任務,存到數據庫中。
# 執行時會從數據庫中取出觸發器來執行,如果觸發器的名稱和執行時間相同,則只有一個節點去執行此任務。
# for cluster
org.quartz.jobStore.tablePrefix: XXL_JOB_QRTZ_
org.quartz.jobStore.class: org.quartz.impl.jdbcjobstore.JobStoreTX
# 默認是內存
#org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
org.quartz.jobStore.isClustered: true
org.quartz.jobStore.clusterCheckinInterval: 5000
在QuartzJobBean無法注入Service
以下測試都基于MySQL的Quartz集群環境下
前面說到基于MethodInvokingJobDetailFactoryBean創建Job,無法將targetObject和targetMethod參數持久化到數據庫表,因此我們要想辦法將這2個參數存儲到JobDataMap中。
我們利用MethodInvokingJobDetailFactoryBean動態抽象構造Job的思想,將其改造一番。
public class MyMethodInvokingJobBean extends QuartzJobBean implements ApplicationContextAware {
private String targetObject;
private String targetMethod;
private ApplicationContext applicationContext;
public void setTargetObject(String targetObject) {
this.targetObject = targetObject;
}
public void setTargetMethod(String targetMethod) {
this.targetMethod = targetMethod;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
Object object = applicationContext.getBean(this.targetObject);
System.out.println("targetObject:" + targetObject);
System.out.println("targetMethod:" + targetMethod);
try {
Method method = object.getClass().getMethod(this.targetMethod, new Class[] {});
method.invoke(object, new Object[]{});
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
@Configuration
public class MyQuartzConfiguration {
@Autowired
private MyQuartzTask myQuartzTask;
@Bean(name = "myQuartzJobBeanJobDetail")
public JobDetailFactoryBean myQuartzJobBeanJobDetail() {
JobDetailFactoryBean jobDetailFactoryBean = new JobDetailFactoryBean();
jobDetailFactoryBean.setJobClass(MyQuartzJobBean.class);
jobDetailFactoryBean.setDurability(true);
jobDetailFactoryBean.setRequestsRecovery(true);
return jobDetailFactoryBean;
}
@Bean(name = "myQuartzTaskJobDetail")
public JobDetailFactoryBean myQuartzTaskJobDetail() {
JobDetailFactoryBean jobDetailFactoryBean = new JobDetailFactoryBean();
jobDetailFactoryBean.setRequestsRecovery(true);
jobDetailFactoryBean.setDurability(true);
jobDetailFactoryBean.getJobDataMap().put("targetObject", "MyQuartzTask");
jobDetailFactoryBean.getJobDataMap().put("targetMethod", "execute");
jobDetailFactoryBean.setJobClass(MyMethodInvokingJobBean.class);
return jobDetailFactoryBean;
}
}
public class MyQuartzJobBean extends QuartzJobBean {
@Autowired
private MyQuartzService myQuartzService;
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
System.out.println("MyQuartzJobBean executeInternal");
myQuartzService.print();
}
}
@Component(value = "MyQuartzTask")
public class MyQuartzTask {
@Autowired
private MyQuartzService myQuartzService;
public void execute() {
System.out.println("MyQuartzTask");
myQuartzService.print();
}
}
@Service
public class MyQuartzService {
public void print() {
System.out.println("MyQuartzService");
}
}
我們運行測試用例中的testMethodInvokingJobBean()
方法, 發現運行沒問題,已經成功的注入了MyQuartzService
。大致的思想是:抽象出一個MyMethodInvokingJobBean
,注入targetObject和targetMethod參數,利用反射去執行目標類的目標方法,達到動態執行任務的目的,大大降低代碼的耦合度。
public class QuartzTest extends AbstractSpringMvcTest {
@Autowired
private Scheduler scheduler;
@Resource(name = "myQuartzJobBeanJobDetail")
private JobDetail myQuartzJobBeanJobDetail;
@Resource(name = "myQuartzTaskJobDetail")
private JobDetail myQuartzTaskJobDetail;
@Test
public void testMethodInvokingJobBean() throws SchedulerException, InterruptedException {
TriggerKey triggerKey = TriggerKey.triggerKey("simpleTrigger", "simpleTriggerGroup");
SimpleScheduleBuilder simpleScheduleBuilder =
SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(1)
.withRepeatCount(1);
SimpleTrigger simpleTrigger = (SimpleTrigger) TriggerBuilder.newTrigger()
.withIdentity(triggerKey)
.startNow()
.withSchedule(simpleScheduleBuilder)
.build();
scheduler.scheduleJob(myQuartzTaskJobDetail, simpleTrigger);
TimeUnit.MINUTES.sleep(10);
}
@Test
public void testJobDetailFactoryBean() throws InterruptedException, SchedulerException {
TriggerKey triggerKey = TriggerKey.triggerKey("simpleTrigger1", "simpleTriggerGroup");
SimpleScheduleBuilder simpleScheduleBuilder =
SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(1)
.withRepeatCount(1);
SimpleTrigger simpleTrigger = (SimpleTrigger) TriggerBuilder.newTrigger()
.withIdentity(triggerKey)
.withSchedule(simpleScheduleBuilder)
.build();
scheduler.scheduleJob(myQuartzJobBeanJobDetail, simpleTrigger);
TimeUnit.MINUTES.sleep(10);
}
}
當我們運行testJobDetailFactoryBean()
時,此時我們的JobClass是MyQuartzJobBean
。當我們的job類中方法要被執行的時候,Quartz會根據JobClass重新實例化一個對象,這里對象中的屬性都會為空,所以會拋出NPE異常。
看到這里有沒有覺得這個問題好熟悉,在上一篇文章中為什么我的HibernateDaoSupport沒有注入SessionFactory我們有提到過。當Quartz重新實例化對象后,肯定是沒有調用populate()方法。我們此時應該找出當前對象中的所有屬性,然后一一注入。(這里會涉及到AutowiredAnnotationBeanPostProcessor中的postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName)
和postProcessPropertyValues( PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeanCreationException
方法獲取對象中需要被注入的屬性和在Spring容器中獲取相應的屬性值)
我們在SpringBeanJobFactory
中的createJobInstance()中可以看到,就是從這里開始實例化Job類,并且把JobDataMap中的鍵值對填充到實例化后的Job對象中。
我們要做的是重寫SpringBeanJobFactory
中的createJobInstance()
方法,為實例化后的Job對象注入依賴對象。
@Component
public class AutowireSpringBeanJobFactory extends SpringBeanJobFactory implements ApplicationContextAware {
private transient AutowireCapableBeanFactory autowireCapableBeanFactory;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.autowireCapableBeanFactory = applicationContext.getAutowireCapableBeanFactory();
}
@Override
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
Object job = super.createJobInstance(bundle);
autowireCapableBeanFactory.autowireBean(job);
return job;
}
}
AutowireCapableBeanFactory中的autowireBean()方法中是調用populateBean()
完成屬性的注入。
重新配置SchedulerFactoryBean
最后重新運行testJobDetailFactoryBean()
,發現在MyQuartzJobBean
中成功注入了MyQuartzService
。
尾言
飯要慢慢吃,路要慢慢走!