xxl-job中關于quartz中的配置詳解

在半個月之前,有幸看了xxl-job源碼,原本打算寫一篇源碼分析文章。結果由于瑣碎的事情干擾了,擱淺了。本篇文章先預熱一下,講下xxl-job中關于quartz知識。(本文內容參考自xxl-job官網)


xxl-job設計思想和調度模塊剖析

xxl-job將調度行為抽象形成"調度中心"公共平臺,而平臺自身并不承擔業務邏輯,"調度中心"負責發起調度請求。將任務抽象成分散的JobHandler,交由"執行器"統一管理,"執行器"負責接收調度請求并執行對應的JobHandler中業務邏輯。因此,"調度"和"任務"兩部分可以相互解耦,提高系統整體穩定性和擴展性。


xxl-job架構圖.png
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定期檢查是否失效的時候,保證只有一個節點去處理已經失效的schedulerTRIGGER_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參數無法持久化到底層數據庫表。

MethodInvokingJobDetailFactoryBean.png

最終還是根據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服務被中止后,再次啟動或集群中其他機器接手任務時會嘗試恢復執行之前未完成的所有任務。

JobDetailFactoryBean.png

集群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

quartz執行圖.png

在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);
    }
}
testMethodInvokingJobBean.png

當我們運行testJobDetailFactoryBean()時,此時我們的JobClass是MyQuartzJobBean。當我們的job類中方法要被執行的時候,Quartz會根據JobClass重新實例化一個對象,這里對象中的屬性都會為空,所以會拋出NPE異常。

image.png

看到這里有沒有覺得這個問題好熟悉,在上一篇文章中為什么我的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對象中。

image.png

我們要做的是重寫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()完成屬性的注入。

image.png

重新配置SchedulerFactoryBean

image.png

最后重新運行testJobDetailFactoryBean(),發現在MyQuartzJobBean中成功注入了MyQuartzService

image.png

xxl_job_qrtz_job_details.png

尾言

飯要慢慢吃,路要慢慢走!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,936評論 6 535
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,744評論 3 421
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,879評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,181評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,935評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,325評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,384評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,534評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,084評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,892評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,623評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,322評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,735評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,990評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,800評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,084評論 2 375

推薦閱讀更多精彩內容