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

恆宇少年發表於2017-11-15

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

本章目標

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

構建專案

注意:我們本章專案需要結合上一章共同完成,有一點要注意的是任務在持久化到資料庫內時會儲存任務的全路徑,如:com.hengyu.chapter39.timers.GoodStockCheckTimerquartz在執行任務時會根據任務全路徑去執行,如果不一致則會提示找不到指定類,我們本章在建立專案時package需要跟上一章完全一致。

我們這裡就不去直接建立新專案了,直接複製上一章專案的原始碼為新的專案命名為Chapter40

配置分散式

在上一章配置檔案quartz.properties中我們其實已經為分散式做好了相關配置,下面我們就來看一下分散式相關的配置。
分散式相關配置:

1. org.quartz.scheduler.instanceId : 定時任務的例項編號,如果手動指定需要保證每個節點的唯一性,因為quartz不允許出現兩個相同instanceId的節點,我們這裡指定為Auto就可以了,我們把生成編號的任務交給quartz

2. org.quartz.jobStore.isClustered: 這個屬性才是真正的開啟了定時任務的分散式配置,當我們配置為truequartz框架就會呼叫ClusterManager來初始化分散式節點。

3. org.quartz.jobStore.clusterCheckinInterval:配置了分散式節點的檢查時間間隔,單位:毫秒。
下面是quartz.properties配置檔案配置資訊:

#排程器例項名稱
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-second

由於我們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技術交流群

相關文章