分布式任務調度平臺XXL-JOB

為獲得更好的閱讀體驗,請訪問原文:傳送門

一、分布式任務調度概述


什么是任務調度平臺

任務調度是指基于給定的時間點,給定的時間間隔又或者給定執行次數自動的執行任務。我們可以思考一下在以下場景中,我們應該怎么實現:

  • 支付系統每天凌晨 1 點,進行一天清算,每月 1 號進行上個月清算;
  • 電商整點搶購,商品價格8點整開始優惠
  • 12306 購票系統,超過 30 分鐘沒有成功支付訂單的,進行回收處理

為什么需要任務調度平臺

定時任務是程序員不可避免的話題,很多業務場景需要我們某一特定的時刻去做某件任務。一般來說,系統可以使用消息傳遞代替部分定時任務(比如商品成功發貨后,需要向客戶發送短信提醒),兩者有很多相似之處,一些場景下也可以相互替換,但是有一些不能:

  • 時間驅動/ 事件驅動: 內部系統一般可以通過事件來驅動,但如果涉及到外部系統,則只能使用時間驅動。如爬取外部網站價格,每小時爬一次。
  • 批量處理/ 逐條處理: 批量處理堆積的數據更加高效,在不需要實時性的情況下比消息中間件更有優勢。而且有的業務邏輯只能批量處理,如移動每個月結算我們的花費。
  • 實時性/ 非實時性: 消息中間件能夠做到實時處理數據,但是有些情況下并不需要實時,比如:vip 升級。
  • 系統內部/ 系統解耦: 定時任務調度一般是在系統內部,而消息中間件可用于兩個系統間

并且對于分布式系統來說,如果處理不當,會存在同一系統不同節點之間定時任務相互影響的問題,再考慮上監控、日志、信息面板,加上不同系統之間管理維護的問題,自己實現一套的成本又上來了..所以我們可以考慮一些比較成熟的任務調度平臺來使用。

任務調度框架選型

Java 領域主要分布式調度系統如下:

  1. xxl-job:是一個輕量級分布式任務調度平臺,其核心設計目標是開發迅速、學習簡單、輕量級、易擴展 。
  2. Elastic-Job: 當當開源的分布式調度解決方案,由兩個相互獨立的子項目Elastic-Job-Lite和Elastic-Job-Cloud組成;Elastic-Job-Lite定位為輕量級無中心化解決方案,使用jar包的形式提供分布式任務的協調服務;Elastic-Job-Cloud采用自研Mesos Framework的解決方案,額外提供資源治理、應用分發以及進程隔離等功能;
  3. Saturn:是唯品會開源的一個分布式任務調度平臺,在當當開源的Elastic Job基礎上,取代傳統的Linux Cron/Spring Batch Job的方式,做到全域統一配置,統一監控,任務高可用以及分片并發處理;
  4. light-task-scheduler:阿里員工開源的個人項目,主要用于解決分布式任務調度問題,支持實時任務,定時任務和Cron任務。有較好的伸縮性,擴展性,健壯穩定性
  5. Quartz: Java定時任務的標配。利用數據庫的鎖機制實現集群調度,業務代碼需要考慮調度的邏輯,對業務代碼有入侵。

在這之前,我是一個都不知道的..有很多文章對他們進行對比,我們就參考其中一篇(下 2),選擇熱門且成熟的 XXL-JOB 來上手研究一下。

二、XXL-JOB


概述

官方中文文檔:http://www.xuxueli.com/xxl-job/

XXL-JOB是一個輕量級分布式任務調度平臺,其核心設計目標是開發迅速、學習簡單、輕量級、易擴展。現已開放源代碼并接入多家公司線上產品線,開箱即用。

快速入門 - 本地運行

先定個小目標,先把它在本地跑起來先。

第一步:下載代碼到本地

找一個合適的目錄,然后執行下列語句把代碼下載到本地:

$ git clone https://github.com/xuxueli/xxl-job.git

第二步:執行初始化 SQL,再用 IDEA 打開

找到 /xxl-job/doc/db/table_xxl_job.sql 初始化 SQL 腳本,并在本地執行。

然后按照 Maven 格式將源碼導入 IDEA,源碼結構如下:

xxl-job-admin:調度中心
xxl-job-core:公共依賴
xxl-job-executor-samples:執行器Sample示例(選擇合適的版本執行器,可直接使用,也可以參考其并將現有項目改造成執行器)
    :xxl-job-executor-sample-springboot:Springboot版本,通過Springboot管理執行器,推薦這種方式;
    :xxl-job-executor-sample-spring:Spring版本,通過Spring容器管理執行器,比較通用;
    :xxl-job-executor-sample-frameless:無框架版本;
    :xxl-job-executor-sample-jfinal:JFinal版本,通過JFinal管理執行器;
    :xxl-job-executor-sample-nutz:Nutz版本,通過Nutz管理執行器;

第三步:配置并啟動 "調度中心"

調度中心配置文件地址:

/xxl-job/xxl-job-admin/src/main/resources/xxl-job-admin.properties

調度中心配置內容說明:

### 調度中心JDBC鏈接:鏈接地址請保持和初始化時創建的數據庫保持一致
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxl_job?Unicode=true&characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=root_pwd
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

### 報警郵箱
spring.mail.host=smtp.qq.com
spring.mail.port=25
spring.mail.username=xxx@qq.com
spring.mail.password=xxx
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory

### xxl-job, access token
xxl.job.accessToken=

### xxl-job, i18n (default empty as chinese, "en" as english)
xxl.job.i18n=

在第一次啟動的項目的時候可能會遇到找不到 log 文件的錯誤(Failed to create),我們只需要自己手動創建一下就好了,具體可以參照這篇文章:https://blog.csdn.net/leeue/article/details/100779424,記得之后再手動把當前目錄權限置為可寫狀態哦:sudo chmod 777 xxl-job

當一切配置好了之后,我們就可以啟動項目了,調度中心訪問地址:http://localhost:8080/xxl-job-admin(該地址執行期將會使用到,作為回調地址),默認登錄賬號 "admin/123456",登錄后運行界面如下圖所示:

至此,「調度中心」項目已經部署成功了,調度中心集群(可選)配置可參考官方文檔。

第四步:配置啟動"執行器"

執行器配置,配置文件地址:

/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/resources/application.properties

執行器配置,配置內容說明:

### 調度中心部署跟地址 [選填]:如調度中心集群部署存在多個地址則用逗號分隔。執行器將會使用該地址進行"執行器心跳注冊"和"任務結果回調";為空則關閉自動注冊;
xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin

### 執行器AppName [選填]:執行器心跳注冊分組依據;為空則關閉自動注冊
xxl.job.executor.appname=xxl-job-executor-sample

### 執行器IP [選填]:默認為空表示自動獲取IP,多網卡時可手動設置指定IP,該IP不會綁定Host僅作為通訊實用;地址信息用于 "執行器注冊" 和 "調度中心請求并觸發任務";
xxl.job.executor.ip=

### 執行器端口號 [選填]:小于等于0則自動獲取;默認端口為9999,單機部署多個執行器時,注意要配置不同執行器端口;
xxl.job.executor.port=9999

### 執行器通訊TOKEN [選填]:非空時啟用;(注意與調度中心保持一致)
xxl.job.accessToken=

### 執行器運行日志文件存儲磁盤路徑 [選填] :需要對該路徑擁有讀寫權限;為空則使用默認路徑;
xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler

### 執行器日志保存天數 [選填] :值大于3時生效,啟用執行器Log文件定期清理功能,否則不生效;
xxl.job.executor.logretentiondays=-1

同樣,也要注意一下日志文件的創建和權限問題,解決方法同上。

當配置完成之后運行起來,我們就可以在剛才的任務調度中心的主頁,在右上角的「執行器的數量」上 + 1 了。

第五步:開發第一個任務

當「調度中心」和「執行器」都啟動之后,我們可以直接在「調度中心」的任務管理界面新增一條配置如下圖所示(參考)的任務:

我們點擊「操作」按鈕下的「GLUE IDE」可以手動編寫我們要執行的腳本,我們可以把我們的任務代碼改寫成如下的樣子:

package com.xxl.job.service.handler;

import com.xxl.job.core.log.XxlJobLogger;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.handler.IJobHandler;
import java.util.concurrent.TimeUnit;

public class DemoGlueJobHandler extends IJobHandler {

    @Override
    public ReturnT<String> execute(String param) throws Exception {
        XxlJobLogger.log("XXL-JOB, Hello World.");
        for (int i = 0; i < 5; i++) {
            XxlJobLogger.log("beat at:" + i);
            TimeUnit.SECONDS.sleep(2);
        }
        return SUCCESS;
    }
}

點擊「保存」,然后繼續在「操作」按鈕下點擊「執行一次」的操作,就可以在「調度日志」中看到我們的任務執行情況啦:

可以看到默認執行器中的日志輸出了:

回頭理解一下過程

到目前為止,我們整個搭建運行的過程都比較順滑,沒有出現什么阻礙,現在我們稍微來理解一下這個過程。

首先我們在本地初始化了框架提供的 SQL 語句,里面定義的結構足夠我們不管是單機還是分布式的任務管理需求。然后我們簡單配置了一下連接的數據庫、報警郵件、token 等信息成功啟動了「調度中心」項目。這個時候項目中默認注冊一個名字為 xxl-job-exectutor-sample 的執行器(名字同默認執行器的 AppName),并且采用的是自動注冊的方式。

等我們把執行器配置項里的 xxl,job.admin.addresses 填寫上「調度中心」實際的地址,然后 token 保持與「調度中心」一致,啟動執行器時,執行器就會把自身的一些基礎信息發送給「調度中心」,這時候「調度中心」會把接收到的注冊信息與自身注冊列表里的 AppName 進行對比(AppName 是每一個執行器的唯一標示),有匹配時就會把 ip 自動填寫上(多個節點就寫多個地址),并在 xxl_job_registry 表上更新信息。執行器可以簡單理解為項目內嵌了端口為 9999(默認端口)的一個 Server。(架構圖如下)

任務 "運行模式"

在剛才的「快速入門」中,我們新建了一個「GLUE模式(Java)」模式的任務,我們在新建任務時可以直接在「調度中心」上編輯代碼,然后讓我們的 ”執行器“ 執行,這樣的一種模式是把代碼直接放在「調度中心」的做法,它的原理是:每個 "GLUE模式(Java)" 任務的代碼,實際上是“一個繼承自 “IJobHandler” 的實現類的類代碼”,“執行器”接收到“調度中心”的調度請求時,會通過 Groovy 類加載器加載此代碼,實例化成 Java 對象,同時注入此代碼中聲明的 Spring 服務(請確保 Glue 代碼中的服務和類引用在“執行器”項目中存在),然后調用該對象的 execute 方法,執行任務邏輯。

另外一種方式是你提前把代碼寫進「執行器」程序中,這樣的模式在 XXL-JOB 中叫做「Bean模式」:每個 Bean 模式任務都是一個 Spring 的 Bean 類實例,它被維護在“執行器”項目的 Spring 容器中。任務類需要加 “@JobHandler(value="名稱")” 注解,因為“執行器”會根據該注解識別 Spring 容器中的任務。任務類需要繼承統一接口 “IJobHandler”,任務邏輯在 execute 方法中開發,因為“執行器”在接收到調度中心的調度請求時,將會調用 “IJobHandler” 的 execute 方法,執行任務邏輯。

例如在 XXL-JOB 提供的實例代碼中就有下面這么一段兒:

package com.xxl.job.executor.service.jobhandler;

import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.handler.IJobHandler;
import com.xxl.job.core.handler.annotation.JobHandler;
import com.xxl.job.core.log.XxlJobLogger;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;


/**
 * 任務Handler示例(Bean模式)
 *
 * 開發步驟:
 * 1、繼承"IJobHandler":“com.xxl.job.core.handler.IJobHandler”;
 * 2、注冊到Spring容器:添加“@Component”注解,被Spring容器掃描為Bean實例;
 * 3、注冊到執行器工廠:添加“@JobHandler(value="自定義jobhandler名稱")”注解,注解value值對應的是調度中心新建任務的JobHandler屬性的值。
 * 4、執行日志:需要通過 "XxlJobLogger.log" 打印執行日志;
 *
 * @author xuxueli 2015-12-19 19:43:36
 */
@JobHandler(value="demoJobHandler")
@Component
public class DemoJobHandler extends IJobHandler {

    @Override
    public ReturnT<String> execute(String param) throws Exception {
        XxlJobLogger.log("XXL-JOB, Hello World.");

        for (int i = 0; i < 5; i++) {
            XxlJobLogger.log("beat at:" + i);
            TimeUnit.SECONDS.sleep(2);
        }
        return SUCCESS;
    }
}

我們就能在創建任務時直接按照下圖這樣創建,那么在調用任務時,"執行器" 就能夠如愿的執行上面的邏輯:

當然 XXL-JOB 還能支持一些腳本語言類型的模式:

- shell腳本:任務運行模式選擇為 "GLUE模式(Shell)"時支持 "shell" 腳本任務;
- python腳本:任務運行模式選擇為 "GLUE模式(Python)"時支持 "python" 腳本任務;
- nodejs腳本:務運行模式選擇為 "GLUE模式(NodeJS)"時支持 "nodejs" 腳本任務;

三、接入指南


  • 前提:已經搭建并成功運行了「調度中心」服務。

快速接入

第一步,我們需要在 pom 文件中引入 xxl-job-core 的 Maven 依賴,不過比較奇怪的是,明明 Github 上最新版本是 2.1.1,Maven 倉庫上卻沒有最新的包,所以只能用 2.1.0 的:

<dependency>
  <groupId>com.xuxueli</groupId>
  <artifactId>xxl-job-core</artifactId>
  <version>2.1.0</version>
</dependency>

第二步,在配置文件中加入 xxl 相關的配置文件信息,不管 yml 格式還是 properties 都行,上面提供了 properties 的版本,這了就提供一個 yml 格式的作參考吧:

xxl:
  job:
    accessToken: xxxx
    admin:
      addresses: http://127.0.0.1:8080/xxl-job-admin
    executor:
      appname: test
      logpath: /data/applogs/xxl-job/jobhandler
      logretentiondays: -1
      ip:
      port: 9999

第三步,在合適的包目錄下新建 XxlJobConfig 配置類:

@Configuration
public class XxlJobConfig {
    private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);

    @Value("${xxl.job.admin.addresses}")
    private String adminAddresses;

    @Value("${xxl.job.executor.appname}")
    private String appName;

    @Value("${xxl.job.executor.ip}")
    private String ip;

    @Value("${xxl.job.executor.port}")
    private int port;

    @Value("${xxl.job.accessToken}")
    private String accessToken;

    @Value("${xxl.job.executor.logpath}")
    private String logPath;

    @Value("${xxl.job.executor.logretentiondays}")
    private int logRetentionDays;

    @Bean(initMethod = "start", destroyMethod = "destroy")
    public XxlJobSpringExecutor xxlJobExecutor() {
        logger.info(">>>>>>>>>>> xxl-job config init.");
        XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
        xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
        xxlJobSpringExecutor.setAppName(appName);
        xxlJobSpringExecutor.setIp(ip);
        xxlJobSpringExecutor.setPort(port);
        xxlJobSpringExecutor.setAccessToken(accessToken);
        xxlJobSpringExecutor.setLogPath(logPath);
        xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);

        return xxlJobSpringExecutor;
    }

    /**
     * 針對多網卡、容器內部署等情況,可借助 "spring-cloud-commons" 提供的 "InetUtils" 組件靈活定制注冊IP;
     *
     *      1、引入依賴:
     *          <dependency>
     *             <groupId>org.springframework.cloud</groupId>
     *             <artifactId>spring-cloud-commons</artifactId>
     *             <version>${version}</version>
     *         </dependency>
     *
     *      2、配置文件,或者容器啟動變量
     *          spring.cloud.inetutils.preferred-networks: 'xxx.xxx.xxx.'
     *
     *      3、獲取IP
     *          String ip_ = inetUtils.findFirstNonLoopbackHostInfo().getIpAddress();
     */
}

至此,我們的項目就差不多完成了我們的接入工作了,就只剩下開發 Handler 的工作量了。

第四步,建一個示例 DemoJobHandler 在平臺上自測一下:

@JobHandler(value="demoJobHandler")
@Component
public class DemoJobHandler extends IJobHandler {

    @Override
    public ReturnT<String> execute(String param) throws Exception {
        XxlJobLogger.log("XXL-JOB, Hello World.");

        for (int i = 0; i < 5; i++) {
            XxlJobLogger.log("beat at:" + i);
            TimeUnit.SECONDS.sleep(2);
        }
        return SUCCESS;
    }
}

然后我們可以啟動項目,看看「調度中心」是否已經成功注冊當前項目的「執行器」,再使用上面介紹的「新建任務」的方法,來測試一下是否正常接入。

小結

總體來說 XXL-JOB 非常的容易上手,并且官方提供了很友好的實例代碼,包括一些高級特性「分片」、「遠程調用」等多種任務都能夠很好的通過示例代碼理解和使用,這里就不再詳細贅述了..官方文檔已經很完善了,感興趣的小伙伴可以去閱讀以下。

參考資料


  1. https://www.expectfly.com/2017/08/15/%E5%88%86%E5%B8%83%E5%BC%8F%E5%AE%9A%E6%97%B6%E4%BB%BB%E5%8A%A1%E6%96%B9%E6%A1%88%E6%8A%80%E6%9C%AF%E9%80%89%E5%9E%8B/ - 分布式定時任務調度系統選型
  2. https://www.yzhu.name/2019/03/30/Schedule-Job/ - 分布式調度系統選型
  3. https://blog.csdn.net/qq924862077/article/details/82708610 - XXL-JOB原理--執行器注冊(二)

按照慣例黏一個尾巴:

歡迎轉載,轉載請注明出處!
獨立域名博客:wmyskxz.com
簡書ID:@我沒有三顆心臟
github:wmyskxz
歡迎關注公眾微信號:wmyskxz
分享自己的學習 & 學習資料 & 生活
想要交流的朋友也可以加qq群:3382693

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

推薦閱讀更多精彩內容