第四十章:基於SpringBoot & Quartz完成定時任務分佈式多節點負載持久化

在上一章【第三十九章:基於SpringBoot & Quartz完成定時任務分佈式單節點持久化】中咱們已經完成了任務的持久化,當咱們建立一個任務時任務會被quartz定時任務框架自動持久化到數據庫,咱們採用的是SpringBoot項目託管的dataSource來完成的數據源提供,固然也可使用quartz內部配置數據源方式,咱們的標題既然是提到了定時任務的分佈式多節點,那麼怎麼纔算是多節點呢?當有節點故障或者手動中止運行後是否能夠自動漂移任務到可用的分佈式節點呢?node

本章目標

  1. 完成定時任務分佈式多節點配置,當單個節點關閉時其餘節點自動接管定時任務。
  2. 建立任務時傳遞自定義參數,方便任務處理後續業務邏輯。

構建項目

注意:咱們本章項目須要結合上一章共同完成,有一點要注意的是任務在持久化到數據庫內時會保存任務的全路徑,如:com.hengyu.chapter39.timers.GoodStockCheckTimerquartz在運行任務時會根據任務全路徑去執行,若是不一致則會提示找不到指定類,咱們本章在建立項目時package須要跟上一章徹底一致。git

咱們這裏就不去直接建立新項目了,直接複製上一章項目的源碼爲新的項目命名爲Chapter40spring

配置分佈式

在上一章配置文件quartz.properties中咱們其實已經爲分佈式作好了相關配置,下面咱們就來看一下分佈式相關的配置。
分佈式相關配置:數據庫

1. org.quartz.scheduler.instanceId : 定時任務的實例編號,若是手動指定須要保證每一個節點的惟一性,由於quartz不容許出現兩個相同instanceId的節點,咱們這裏指定爲Auto就能夠了,咱們把生成編號的任務交給quartzbash

2. org.quartz.jobStore.isClustered: 這個屬性纔是真正的開啓了定時任務的分佈式配置,當咱們配置爲truequartz框架就會調用ClusterManager來初始化分佈式節點。併發

3. org.quartz.jobStore.clusterCheckinInterval:配置了分佈式節點的檢查時間間隔,單位:毫秒。
下面是quartz.properties配置文件配置信息:app

#調度器實例名稱
org.quartz.scheduler.instanceName = quartzScheduler

#調度器實例編號自動生成
org.quartz.scheduler.instanceId = AUTO

#持久化方式配置
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX

#持久化方式配置數據驅動,MySQL數據庫
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate

#quartz相關數據表前綴名
org.quartz.jobStore.tablePrefix = QRTZ_

#開啓分佈式部署
org.quartz.jobStore.isClustered = true
#配置是否使用
org.quartz.jobStore.useProperties = false

#分佈式節點有效性檢查時間間隔,單位:毫秒
org.quartz.jobStore.clusterCheckinInterval = 10000

#線程池實現類
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool

#執行最大併發線程數量
org.quartz.threadPool.threadCount = 10

#線程優先級
org.quartz.threadPool.threadPriority = 5

#配置爲守護線程,設置後任務將不會執行
#org.quartz.threadPool.makeThreadsDaemons=true

#配置是否啓動自動加載數據庫內的定時任務,默認true
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true複製代碼

當咱們啓動任務節點時,會根據org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread屬性配置進行是否自動加載任務,默認true自動加載數據庫內的任務到節點。框架

測試分佈式

上一章項目節點名稱:quartz-cluster-node-first
本章項目節點名稱:quartz-cluster-node-seconddom

因爲咱們quartz-cluster-node-first的商品庫存檢查定時任務是每隔30秒執行一次,因此任務除非手動清除不然是不會被清空的,在運行項目測試以前須要將application.yml配置文件的端口號、項目名稱修改下,保證quartz-cluster-node-secondquartz-cluster-node-first端口號不一致,能夠同時運行,修改後爲:分佈式

spring:
    application:
        name: quzrtz-cluster-node-second
server:
  port: 8082複製代碼

而後修改相應控制檯輸出,爲了可以區分任務執行者是具體的節點。

Chapter40Application啓動類修改日誌輸出:
logger.info("【【【【【【定時任務分佈式節點 - quartz-cluster-node-second 已啓動】】】】】】");

GoodAddTimer商品添加任務類修改日誌輸出:
logger.info("分佈式節點quartz-cluster-node-second,商品添加完成後執行任務,任務時間:{}",new Date());

GoodStockCheckTimer商品庫存檢查任務類修改日誌輸出:
logger.info("分佈式節點quartz-cluster-node-second,執行庫存檢查定時任務,執行時間:{}",new Date());複製代碼

下面咱們啓動本章項目,查看控制檯輸出內容,以下所示:

2017-11-12 10:28:39.969  INFO 11048 --- [           main] c.hengyu.chapter39.Chapter40Application  : 【【【【【【定時任務分佈式節點 - quartz-cluster-node-second 已啓動】】】】】】
2017-11-12 10:28:41.930  INFO 11048 --- [lerFactoryBean]] o.s.s.quartz.SchedulerFactoryBean        : Starting Quartz Scheduler now, after delay of 2 seconds
2017-11-12 10:28:41.959  INFO 11048 --- [lerFactoryBean]] org.quartz.core.QuartzScheduler          : Scheduler schedulerFactoryBean_$_yuqiyu1510453719308 started.
2017-11-12 10:28:51.963  INFO 11048 --- [_ClusterManager] o.s.s.quartz.LocalDataSourceJobStore     : ClusterManager: detected 1 failed or restarted instances.
2017-11-12 10:28:51.963  INFO 11048 --- [_ClusterManager] o.s.s.quartz.LocalDataSourceJobStore     : ClusterManager: Scanning for instance "yuqiyu1510450938654"'s failed in-progress jobs. 2017-11-12 10:28:51.967 INFO 11048 --- [_ClusterManager] o.s.s.quartz.LocalDataSourceJobStore : ClusterManager: ......Freed 1 acquired trigger(s). 2017-11-12 10:29:00.024 INFO 11048 --- [ryBean_Worker-1] c.h.c.timers.GoodStockCheckTimer : 分佈式節點quartz-cluster-node-second,執行庫存檢查定時任務,執行時間:Sun Nov 12 10:29:00 CST 2017複製代碼

能夠看到項目啓動完成後自動分配的instanceIdyuqiyu1510450938654,生成的規則是當前用戶的名稱+時間戳。而後ClusterManager分佈式管理者自動介入進行掃描是否存在匹配的觸發器任務,若是存在則會自動執行任務邏輯,而商品庫存檢查定時任務確實由quartz-cluster-node-second進行輸出的。

測試任務自動漂移

下面咱們也須要把quartz-cluster-node-first的輸出進行修改,以下所示:

Chapter39Application啓動類修改日誌輸出:
logger.info("【【【【【【定時任務分佈式節點 - quartz-cluster-node-first 已啓動】】】】】】");

GoodAddTimer商品添加任務類修改日誌輸出:
logger.info("分佈式節點quartz-cluster-node-first,商品添加完成後執行任務,任務時間:{}",new Date());

GoodStockCheckTimer商品庫存檢查任務類修改日誌輸出:
logger.info("分佈式節點quartz-cluster-node-first,執行庫存檢查定時任務,執行時間:{}",new Date());複製代碼

接下來咱們啓動quartz-cluster-node-first,並查看控制檯的輸出內容:

2017-11-12 10:34:09.750  INFO 192 --- [           main] c.hengyu.chapter39.Chapter39Application  : 【【【【【【定時任務分佈式節點 - quartz-cluster-node-first 已啓動】】】】】】
2017-11-12 10:34:11.690  INFO 192 --- [lerFactoryBean]] o.s.s.quartz.SchedulerFactoryBean        : Starting Quartz Scheduler now, after delay of 2 seconds
2017-11-12 10:34:11.714  INFO 192 --- [lerFactoryBean]] org.quartz.core.QuartzScheduler          : Scheduler schedulerFactoryBean_$_yuqiyu1510454049066 started.複製代碼

項目啓動完成後,定時節點並無實例化ClusterManager來完成分佈式節點的初始化,由於quartz檢測到有其餘的節點正在處理任務,這樣也是保證了任務執行的惟一性。

關閉quartz-cluster-node-second

咱們關閉quartz-cluster-node-second運行的項目,預計的目的能夠達到quartz-cluster-node-first會自動接管數據庫內的任務,完成任務執行的自動漂移,咱們來查看quartz-cluster-node-first的控制檯輸出內容:

2017-11-12 10:34:09.750  INFO 192 --- [           main] c.hengyu.chapter39.Chapter39Application  : 【【【【【【定時任務分佈式節點 - quartz-cluster-node-first 已啓動】】】】】】
2017-11-12 10:34:11.690  INFO 192 --- [lerFactoryBean]] o.s.s.quartz.SchedulerFactoryBean        : Starting Quartz Scheduler now, after delay of 2 seconds
2017-11-12 10:34:11.714  INFO 192 --- [lerFactoryBean]] org.quartz.core.QuartzScheduler          : Scheduler schedulerFactoryBean_$_yuqiyu1510454049066 started.
2017-11-12 10:41:11.793  INFO 192 --- [_ClusterManager] o.s.s.quartz.LocalDataSourceJobStore     : ClusterManager: detected 1 failed or restarted instances.
2017-11-12 10:41:11.793  INFO 192 --- [_ClusterManager] o.s.s.quartz.LocalDataSourceJobStore     : ClusterManager: Scanning for instance "yuqiyu1510453719308"'s failed in-progress jobs. 2017-11-12 10:41:11.797 INFO 192 --- [_ClusterManager] o.s.s.quartz.LocalDataSourceJobStore : ClusterManager: ......Freed 1 acquired trigger(s). 2017-11-12 10:41:11.834 INFO 192 --- [ryBean_Worker-1] c.h.c.timers.GoodStockCheckTimer : 分佈式節點quartz-cluster-node-first,執行庫存檢查定時任務,執行時間:Sun Nov 12 10:41:11 CST 2017複製代碼

控制檯已經輸出了持久的定時任務,輸出節點是quartz-cluster-node-first,跟咱們預計的同樣,節點quartz-cluster-node-first完成了自動接管quartz-cluster-node-second的工做,而這個過程確定有一段時間間隔,而這個間隔能夠修改quartz.properties配置文件內的屬性org.quartz.jobStore.clusterCheckinInterval進行調節。

關閉quartz-cluster-node-first

咱們一樣能夠測試啓動任務節點quartz-cluster-node-second後,再關閉quartz-cluster-node-first任務節點,查看quartz-cluster-node-second控制檯的輸出內容:

2017-11-12 10:53:31.010  INFO 3268 --- [           main] c.hengyu.chapter39.Chapter40Application  : 【【【【【【定時任務分佈式節點 - quartz-cluster-node-second 已啓動】】】】】】
2017-11-12 10:53:32.967  INFO 3268 --- [lerFactoryBean]] o.s.s.quartz.SchedulerFactoryBean        : Starting Quartz Scheduler now, after delay of 2 seconds
2017-11-12 10:53:32.992  INFO 3268 --- [lerFactoryBean]] org.quartz.core.QuartzScheduler          : Scheduler schedulerFactoryBean_$_yuqiyu1510455210493 started.
2017-11-12 10:53:52.999  INFO 3268 --- [_ClusterManager] o.s.s.quartz.LocalDataSourceJobStore     : ClusterManager: detected 1 failed or restarted instances.
2017-11-12 10:53:52.999  INFO 3268 --- [_ClusterManager] o.s.s.quartz.LocalDataSourceJobStore     : ClusterManager: Scanning for instance "yuqiyu1510454049066"'s failed in-progress jobs. 2017-11-12 10:53:53.003 INFO 3268 --- [_ClusterManager] o.s.s.quartz.LocalDataSourceJobStore : ClusterManager: ......Freed 1 acquired trigger(s). 2017-11-12 10:54:00.020 INFO 3268 --- [ryBean_Worker-1] c.h.c.timers.GoodStockCheckTimer : 分佈式節點quartz-cluster-node-second,執行庫存檢查定時任務,執行時間:Sun Nov 12 10:54:00 CST 2017複製代碼

獲得的結果是一樣能夠完成任務的自動漂移。

若是兩個節點同時啓動,哪一個節點先把節點信息註冊到數據庫就得到了優先執行權。

傳遞參數到任務

咱們平時在使用任務時,若是是針對性比較強的業務邏輯,確定須要特定的參數來完成業務邏輯的實現。

下面咱們來模擬商品秒殺的場景,當咱們添加商品後自動添加一個商品提早五分鐘的秒殺提醒,爲關注該商品的用戶發送提醒消息。
咱們在節點quartz-cluster-node-first中添加秒殺提醒任務,以下所示:

package com.hengyu.chapter39.timers;

import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.quartz.QuartzJobBean;

/**
 * 商品秒殺提醒定時器
 * 爲關注該秒殺商品的用戶進行推送提醒
 * ========================
 *
 * @author 恆宇少年
 * Created with IntelliJ IDEA.
 * Date:2017/11/12
 * Time:9:23
 * 碼雲:http://git.oschina.net/jnyqy
 * ========================
 */
public class GoodSecKillRemindTimer
extends QuartzJobBean
{
    /**
     * logback
     */
    private Logger logger = LoggerFactory.getLogger(GoodSecKillRemindTimer.class);

    /**
     * 任務指定邏輯
     * @param jobExecutionContext
     * @throws JobExecutionException
     */
    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        //獲取任務詳情內的數據集合
        JobDataMap dataMap = jobExecutionContext.getJobDetail().getJobDataMap();
        //獲取商品編號
        Long goodId = dataMap.getLong("goodId");

        logger.info("分佈式節點quartz-cluster-node-first,開始處理秒殺商品:{},關注用戶推送消息.",goodId);

        //.../
    }
}複製代碼

在秒殺提醒任務邏輯中,咱們經過獲取JobDetailJobDataMap集合來獲取在建立任務的時候傳遞的參數集合,咱們這裏約定了goodId爲商品的編號,在建立任務的時候傳遞到JobDataMap內,這樣quartz在執行該任務的時候就會自動將參數傳遞到任務邏輯中,咱們也就能夠經過JobDataMap獲取到對應的參數值。

設置秒殺提醒任務

咱們找到節點項目quartz-cluster-node-first中的GoodInfoService,編寫方法buildGoodSecKillRemindTimer設置秒殺提醒任務,以下所示:

/**
     * 構建商品秒殺提醒定時任務
     * 設置五分鐘後執行
     * @throws Exception
     */
    public void buildGoodSecKillRemindTimer(Long goodId) throws Exception
    {
        //任務名稱
        String name = UUID.randomUUID().toString();
        //任務所屬分組
        String group = GoodSecKillRemindTimer.class.getName();
        //秒殺開始時間
        long startTime = System.currentTimeMillis() + 1000 * 5 * 60;
        JobDetail jobDetail = JobBuilder
                .newJob(GoodSecKillRemindTimer.class)
                .withIdentity(name,group)
                .build();

        //設置任務傳遞商品編號參數
        jobDetail.getJobDataMap().put("goodId",goodId);

        //建立任務觸發器
        Trigger trigger = TriggerBuilder.newTrigger().withIdentity(name,group).startAt(new Date(startTime)).build();
        //將觸發器與任務綁定到調度器內
        scheduler.scheduleJob(jobDetail,trigger);
    }複製代碼

咱們模擬秒殺提醒時間是添加商品後的5分鐘,咱們經過調用jobDetail實例的getJobDataMap方法就能夠獲取該任務數據集合,直接調用put方法就能夠進行設置指定key的值,該集合繼承了StringKeyDirtyFlagMap而且實現了Serializable序列化,由於須要將數據序列化到數據庫的qrtz_job_details表內。
修改保存商品方法,添加調用秒殺提醒任務:

/**
     * 保存商品基本信息
     * @param good 商品實例
     * @return
     */
    public Long saveGood(GoodInfoEntity good) throws Exception
    {
        goodInfoRepository.save(good);
        //構建建立商品定時任務
        buildCreateGoodTimer();
        //構建商品庫存定時任務
        buildGoodStockTimer();
        //構建商品描述提醒定時任務
        buildGoodSecKillRemindTimer(good.getId());
        return good.getId();
    }複製代碼

添加測試商品

下面咱們調用節點quartz-cluster-node-first的測試Chapter39ApplicationTests.addGood方法完成商品的添加,因爲咱們的quartz-cluster-node-second項目並無中止,因此咱們在quartz-cluster-node-second項目的控制檯查看輸出內容:

2017-11-12 11:45:00.008  INFO 11652 --- [ryBean_Worker-5] c.h.c.timers.GoodStockCheckTimer         : 分佈式節點quartz-cluster-node-second,執行庫存檢查定時任務,執行時間:Sun Nov 12 11:45:00 CST 2017
2017-11-12 11:45:30.013  INFO 11652 --- [ryBean_Worker-6] c.h.c.timers.GoodStockCheckTimer         : 分佈式節點quartz-cluster-node-second,執行庫存檢查定時任務,執行時間:Sun Nov 12 11:45:30 CST 2017
2017-11-12 11:45:48.230  INFO 11652 --- [ryBean_Worker-7] c.hengyu.chapter39.timers.GoodAddTimer   : 分佈式節點quartz-cluster-node-second,商品添加完成後執行任務,任務時間:Sun Nov 12 11:45:48 CST 2017
2017-11-12 11:46:00.008  INFO 11652 --- [ryBean_Worker-8] c.h.c.timers.GoodStockCheckTimer         : 分佈式節點quartz-cluster-node-second,執行庫存檢查定時任務,執行時間:Sun Nov 12 11:46:00 CST 2017
2017-11-12 11:46:30.016  INFO 11652 --- [ryBean_Worker-9] c.h.c.timers.GoodStockCheckTimer         : 分佈式節點quartz-cluster-node-second,執行庫存檢查定時任務,執行時間:Sun Nov 12 11:46:30 CST 2017
2017-11-12 11:47:00.011  INFO 11652 --- [yBean_Worker-10] c.h.c.timers.GoodStockCheckTimer         : 分佈式節點quartz-cluster-node-second,執行庫存檢查定時任務,執行時間:Sun Nov 12 11:47:00 CST 2017
2017-11-12 11:47:30.028  INFO 11652 --- [ryBean_Worker-1] c.h.c.timers.GoodStockCheckTimer         : 分佈式節點quartz-cluster-node-second,執行庫存檢查定時任務,執行時間:Sun Nov 12 11:47:30 CST 2017
2017-11-12 11:48:00.014  INFO 11652 --- [ryBean_Worker-2] c.h.c.timers.GoodStockCheckTimer         : 分佈式節點quartz-cluster-node-second,執行庫存檢查定時任務,執行時間:Sun Nov 12 11:48:00 CST 2017
2017-11-12 11:48:30.013  INFO 11652 --- [ryBean_Worker-3] c.h.c.timers.GoodStockCheckTimer         : 分佈式節點quartz-cluster-node-second,執行庫存檢查定時任務,執行時間:Sun Nov 12 11:48:30 CST 2017
2017-11-12 11:49:00.010  INFO 11652 --- [ryBean_Worker-4] c.h.c.timers.GoodStockCheckTimer         : 分佈式節點quartz-cluster-node-second,執行庫存檢查定時任務,執行時間:Sun Nov 12 11:49:00 CST 2017
2017-11-12 11:49:30.028  INFO 11652 --- [ryBean_Worker-5] c.h.c.timers.GoodStockCheckTimer         : 分佈式節點quartz-cluster-node-second,執行庫存檢查定時任務,執行時間:Sun Nov 12 11:49:30 CST 2017
2017-11-12 11:49:48.290  INFO 11652 --- [ryBean_Worker-6] c.h.c.timers.GoodSecKillRemindTimer      : 分佈式節點quartz-cluster-node-second,開始處理秒殺商品:15,關注用戶推送消息.
2017-11-12 11:50:00.008  INFO 11652 --- [ryBean_Worker-7] c.h.c.timers.GoodStockCheckTimer         : 分佈式節點quartz-cluster-node-second,執行庫存檢查定時任務,執行時間:Sun Nov 12 11:50:00 CST 2017複製代碼

秒殺任務在添加完成商品後的五分鐘開始執行的,而咱們也正常的輸出了傳遞過去的goodId商品編號的參數,而秒殺提醒任務執行一次後也被自動釋放了。

總結

本章主要是結合上一章完成了分佈式任務的講解,完成了測試持久化的定時任務自動漂移,以及如何向定時任務傳遞參數。固然在實際的開發過程當中,任務建立是須要進行封裝的,目的也是爲了減小一些冗餘代碼以及方面後期統一維護定時任務。

本章源碼已經上傳到碼雲:
SpringBoot配套源碼地址:gitee.com/hengboy/spr…
SpringCloud配套源碼地址:gitee.com/hengboy/spr…
SpringBoot相關係列文章請訪問:目錄:SpringBoot學習目錄
QueryDSL相關係列文章請訪問:QueryDSL通用查詢框架學習目錄
SpringDataJPA相關係列文章請訪問:目錄:SpringDataJPA學習目錄
SpringBoot相關文章請訪問:目錄:SpringBoot學習目錄,感謝閱讀!
歡迎加入QQ技術交流羣,共同進步。

QQ技術交流羣
相關文章
相關標籤/搜索