項(xiàng)目終于用上了 xxl-job!
兄弟們,最近咱們項(xiàng)目終于跟 “定時(shí)任務(wù)” 的那些破事兒徹底說(shuō)拜拜了 ,因?yàn)樵蹅兩狭?xxl-job!
說(shuō)實(shí)話,在沒(méi)? xxl-job 之前,我跟定時(shí)任務(wù)的恩怨情仇能寫(xiě)?本?說(shuō)。最開(kāi)始用 Timer 的時(shí)候,那叫?個(gè)心驚膽戰(zhàn),服務(wù)器?重啟,之前排好的任務(wù)跟失憶了似的,全沒(méi)了;后來(lái)?yè)Q成 ScheduledExecutorService,雖然比 Timer 穩(wěn)點(diǎn),但集群環(huán)境下又掉坑里了 —— 好幾個(gè)節(jié)點(diǎn)同時(shí)跑同一個(gè)任務(wù),數(shù)據(jù)直接亂套,查問(wèn)題的時(shí)候我跟個(gè)偵探似的,對(duì)著日志翻了半宿,最后發(fā)現(xiàn)是沒(méi)做分布式鎖;再后來(lái)試過(guò)自己寫(xiě) quartz 集群,配置文件改得我頭都大了,而且監(jiān)控和日志還得自己搭,折騰半天效果還不咋地。
直到某天跟隔壁項(xiàng)目的老周吃飯,他跟我吹 “我們項(xiàng)目用 xxl-job,定時(shí)任務(wù)那叫一個(gè)絲滑,啥重啟丟失、集群重復(fù)執(zhí)行的問(wèn)題都沒(méi)有,監(jiān)控日志還全給你整得明明白白”,我當(dāng)時(shí)眼睛一亮,回來(lái)就拉著團(tuán)隊(duì)調(diào)研,這不,沒(méi)多久就給項(xiàng)目安排上了,用了快一個(gè)月,真心覺(jué)得 “早用早香”!
今天就跟大家好好嘮嘮 xxl-job,從它是啥、怎么搭、怎么用,到實(shí)際項(xiàng)目里的坑和技巧,全給你們扒得明明白白,保證小白能看懂,老鳥(niǎo)能撿著干貨。
一、先搞明白:xxl-job 到底是個(gè)啥?
可能還有兄弟沒(méi)接觸過(guò) xxl-job,先給大家用大白話科普下。xxl-job 是個(gè)開(kāi)源的分布式任務(wù)調(diào)度框架,作者是許雪里(xxl 就是他名字的首字母縮寫(xiě)),這框架在 GitHub 上星標(biāo)都快 3 萬(wàn)了,國(guó)內(nèi)很多公司都在用,穩(wěn)定性不用多說(shuō)。
簡(jiǎn)單說(shuō),它就是幫咱們搞定 “定時(shí)干活” 的工具,而且是 “分布式” 的 —— 比如你有 10 臺(tái)服務(wù)器,想讓某個(gè)任務(wù)每天凌晨 2 點(diǎn)在其中一臺(tái)上跑,或者讓 10 臺(tái)服務(wù)器分?jǐn)偛煌娜蝿?wù),xxl-job 都能給你安排得明明白白。
對(duì)比咱們之前用的那些 “土方法”,xxl-job 的優(yōu)勢(shì)簡(jiǎn)直太明顯了:
- 不用自己折騰集群:之前自己搭 quartz 集群,要配置數(shù)據(jù)庫(kù)、改數(shù)據(jù)源,還得擔(dān)心節(jié)點(diǎn)同步問(wèn)題,xxl-job 自帶集群支持,改倆配置就行;
- 有現(xiàn)成的監(jiān)控和日志:之前查任務(wù)執(zhí)行情況,得登錄服務(wù)器翻日志文件,xxl-job 在網(wǎng)頁(yè)上就能看任務(wù)執(zhí)行狀態(tài)、失敗原因,甚至能看完整日志,省了我好多時(shí)間;
- 支持多種任務(wù)類型:不光能執(zhí)行 Java 代碼里的方法,還能直接在網(wǎng)頁(yè)上寫(xiě) Groovy 腳本(GLUE 模式),改任務(wù)邏輯不用重新打包部署,簡(jiǎn)直是迭代神器;
- 容錯(cuò)性強(qiáng):任務(wù)執(zhí)行失敗了能自動(dòng)重試,執(zhí)行器掛了調(diào)度中心會(huì)報(bào)警,再也不用半夜被運(yùn)維電話叫醒說(shuō) “任務(wù)沒(méi)跑起來(lái)” 了。
這么說(shuō)吧,之前處理定時(shí)任務(wù)我得 “天天盯著”,現(xiàn)在用上 xxl-job,我基本不用管,偶爾看眼監(jiān)控就行,摸魚(yú)時(shí)間都變多了(這話可別讓領(lǐng)導(dǎo)看見(jiàn))。
二、實(shí)戰(zhàn)第一步:把 xxl-job 搭起來(lái)
光說(shuō)不練假把式,咱們直接上實(shí)戰(zhàn) —— 怎么把 xxl-job 的調(diào)度中心和執(zhí)行器搭起來(lái)。別擔(dān)心,步驟很簡(jiǎn)單,跟著走就行。
2.1 先準(zhǔn)備環(huán)境(這些都是基礎(chǔ),別偷懶)
首先得有這幾樣?xùn)|西,沒(méi)有的先裝上:
- JDK:1.8 及以上(咱們 Java 項(xiàng)目基本都是 1.8,高版本也兼容,別用太老的就行);
- MySQL:5.7 及以上(xxl-job 要存調(diào)度信息、任務(wù)配置這些,得有個(gè)數(shù)據(jù)庫(kù));
- Maven:3.0 及以上(用來(lái)編譯源碼,打包項(xiàng)目);
- 一個(gè) Spring Boot 項(xiàng)目(咱們實(shí)際開(kāi)發(fā)基本都是 Spring Boot,這里就以 Spring Boot 為例,非 Spring Boot 項(xiàng)目后面也會(huì)提一嘴)。
2.2 部署調(diào)度中心(相當(dāng)于 “指揮中心”)
調(diào)度中心就是 xxl-job 的 “大腦”,所有任務(wù)的配置、調(diào)度邏輯都在這兒,還能看監(jiān)控和日志。部署起來(lái)特別簡(jiǎn)單,分三步:
第一步:下載源碼
直接去 GitHub 下 xxl-job 的源碼,地址是:https://github.com/xuxueli/xxl-job 。建議下最新的穩(wěn)定版,別追更到開(kāi)發(fā)版,萬(wàn)一有 bug 就麻煩了。
下完之后解壓,用 IDEA 打開(kāi),項(xiàng)目結(jié)構(gòu)很清晰,主要看這兩個(gè)模塊:
- xxl-job-admin:這就是調(diào)度中心,是個(gè) Spring Boot 項(xiàng)目;
- xxl-job-executor-samples:執(zhí)行器的示例,里面有 Spring Boot、Spring、非 Spring 的示例,咱們后面會(huì)參考這個(gè)寫(xiě)自己的執(zhí)行器。
第二步:初始化數(shù)據(jù)庫(kù)
調(diào)度中心需要數(shù)據(jù)庫(kù)存數(shù)據(jù),源碼里已經(jīng)給咱們準(zhǔn)備好了 SQL 腳本,路徑在xxl-job/doc/db/tables_xxl_job.sql。
打開(kāi) MySQL,新建一個(gè)數(shù)據(jù)庫(kù)(比如叫xxl_job),然后執(zhí)行這個(gè) SQL 腳本。執(zhí)行完之后會(huì)生成 8 張表,不用管這些表是干嘛的,反正 xxl-job 自己會(huì)用,咱們只要保證表建好了就行。
這里提醒一句:別改表名和字段名,不然調(diào)度中心會(huì)報(bào)錯(cuò),到時(shí)候排查起來(lái)很麻煩。
第三步:改配置、編譯、啟動(dòng)
打開(kāi)xxl-job-admin模塊下的application.properties文件,主要改三個(gè)地方:
1.數(shù)據(jù)庫(kù)連接:把spring.datasource.url、spring.datasource.username、spring.datasource.password改成你自己的 MySQL 地址、用戶名和密碼。比如我的配置是這樣的:
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=123456這里注意,URL 后面要加serverTimezone=Asia/Shanghai,不然可能會(huì)報(bào)時(shí)區(qū)錯(cuò)誤,踩過(guò)這個(gè)坑的兄弟應(yīng)該懂。
2.端口號(hào):默認(rèn)是 8080,要是你本地 8080 被占了(比如 Tomcat),就改成別的,比如 8081,改server.port就行。
3.登錄密碼:默認(rèn)的登錄賬號(hào)是 admin,密碼是 123456,要是想改密碼,后面登錄之后在網(wǎng)頁(yè)上改更方便,這里先不用動(dòng)。
改完配置之后,就可以編譯打包了。在 IDEA 的 Terminal 里執(zhí)行mvn clean package -Dmaven.test.skip=true,等編譯完成,在xxl-job-admin/target目錄下會(huì)生成一個(gè) jar 包,比如xxl-job-admin-2.4.0.jar(版本號(hào)可能不一樣)。
然后執(zhí)行java -jar xxl-job-admin-2.4.0.jar啟動(dòng)調(diào)度中心。啟動(dòng)成功之后,打開(kāi)瀏覽器訪問(wèn)http://localhost:8080/xxl-job-admin(端口號(hào)跟你配置的一致),能看到登錄頁(yè)面,輸入 admin/123456 登錄,就說(shuō)明調(diào)度中心部署成功了!
登錄之后的界面很直觀,左邊有任務(wù)管理、執(zhí)行器管理、監(jiān)控報(bào)表這些菜單,后面咱們會(huì)一個(gè)個(gè)用到。
2.3 開(kāi)發(fā)執(zhí)行器(相當(dāng)于 “干活的小弟”)
調(diào)度中心是 “指揮中心”,那執(zhí)行器就是 “干活的小弟”—— 實(shí)際的任務(wù)邏輯(比如同步數(shù)據(jù)、清理日志)都是在執(zhí)行器里寫(xiě)的,執(zhí)行器會(huì)注冊(cè)到調(diào)度中心,等著調(diào)度中心發(fā)指令來(lái)執(zhí)行任務(wù)。
咱們以 Spring Boot 項(xiàng)目為例,寫(xiě)一個(gè)自己的執(zhí)行器,分四步:
第一步:引入依賴
在自己的 Spring Boot 項(xiàng)目的 pom.xml 里,引入 xxl-job 的執(zhí)行器依賴。注意版本要跟調(diào)度中心的版本一致,不然可能有兼容性問(wèn)題:
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>2.4.0</version> <!-- 跟調(diào)度中心版本一致 -->
</dependency>第二步:加配置
在application.yml(或者 application.properties)里加執(zhí)行器的配置,我習(xí)慣用 yml,看著更清晰:
xxl:
job:
admin:
# 調(diào)度中心的地址,要是調(diào)度中心是集群,就用逗號(hào)分隔,比如http://127.0.0.1:8080/xxl-job-admin,http://127.0.0.1:8081/xxl-job-admin
addresses: http://127.0.0.1:8080/xxl-job-admin
executor:
# 執(zhí)行器的名稱,這個(gè)很重要,后面在調(diào)度中心配置任務(wù)的時(shí)候要用到,必須一致
appname: xxl-job-executor-demo
# 執(zhí)行器的IP地址,默認(rèn)不用填,會(huì)自動(dòng)獲取,要是有多網(wǎng)卡可以指定
ip:
# 執(zhí)行器的端口號(hào),默認(rèn)是9999,要是啟動(dòng)多個(gè)執(zhí)行器,端口號(hào)要不一樣
port: 9999
# 執(zhí)行器的日志路徑,默認(rèn)是/data/applogs/xxl-job/jobhandler,也可以自己改
logpath: /data/applogs/xxl-job/jobhandler
# 執(zhí)行器日志的保存天數(shù),默認(rèn)7天,超過(guò)會(huì)自動(dòng)刪除
logretentiondays: 30
# 訪問(wèn)令牌,要是調(diào)度中心配置了令牌,這里就要填一樣的,默認(rèn)沒(méi)有,不用填
accessToken:這里有幾個(gè)坑要注意:
- 執(zhí)行器的 appname 必須跟后面在調(diào)度中心配置的一致,不然執(zhí)行器注冊(cè)不上;
- 要是啟動(dòng)多個(gè)執(zhí)行器(集群),port 必須不一樣,比如第一個(gè) 9999,第二個(gè) 10000;
- 調(diào)度中心的 addresses 要是集群,一定要寫(xiě)全,不然執(zhí)行器可能只注冊(cè)到一個(gè)調(diào)度中心節(jié)點(diǎn)。
第三步:寫(xiě)配置類
新建一個(gè)配置類,比如XxlJobConfig,用來(lái)初始化執(zhí)行器的客戶端,代碼直接抄示例就行,不用改太多:
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@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
public XxlJobSpringExecutor xxlJobExecutor() {
logger.info(">>>>>>>>>>> xxl-job executor 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;
}
}第四步:寫(xiě)實(shí)際的任務(wù)邏輯
這一步是核心,咱們寫(xiě)個(gè)簡(jiǎn)單的任務(wù),比如 “每隔 5 秒打印一次當(dāng)前時(shí)間”,再寫(xiě)個(gè)實(shí)際業(yè)務(wù)中常用的 “定時(shí)同步用戶數(shù)據(jù)” 的任務(wù)。
新建一個(gè)任務(wù)類,比如DemoJobHandler,用@XxlJob注解標(biāo)記任務(wù)方法,注解里的 value 就是任務(wù)的標(biāo)識(shí),后面在調(diào)度中心配置的時(shí)候要用到:
import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.annotation.XxlJob;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
@Component
public class DemoJobHandler {
private static final Logger logger = LoggerFactory.getLogger(DemoJobHandler.class);
// 任務(wù)1:每隔5秒打印當(dāng)前時(shí)間
@XxlJob("printTimeJob")
public void printTimeJob() {
// XxlJobHelper.log()是xxl-job提供的日志方法,打印的日志能在調(diào)度中心看到
XxlJobHelper.log("printTimeJob start...");
try {
// 獲取當(dāng)前時(shí)間
String currentTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
// 打印時(shí)間
logger.info("當(dāng)前時(shí)間:{}", currentTime);
XxlJobHelper.log("當(dāng)前時(shí)間:{}", currentTime);
// 任務(wù)執(zhí)行成功,不用寫(xiě)這個(gè)也會(huì)默認(rèn)成功,要是失敗了要拋異?;蛘哒{(diào)用XxlJobHelper.handleFail()
XxlJobHelper.handleSuccess("任務(wù)執(zhí)行成功");
} catch (Exception e) {
// 任務(wù)執(zhí)行失敗,記錄日志并標(biāo)記失敗
XxlJobHelper.log("任務(wù)執(zhí)行失?。簕}", e.getMessage());
XxlJobHelper.handleFail("任務(wù)執(zhí)行失敗");
}
}
// 任務(wù)2:定時(shí)同步用戶數(shù)據(jù)(模擬實(shí)際業(yè)務(wù))
@XxlJob("syncUserJob")
public void syncUserJob() {
XxlJobHelper.log("syncUserJob start...");
try {
// 1. 獲取需要同步的用戶數(shù)據(jù)(這里模擬從數(shù)據(jù)庫(kù)查詢)
XxlJobHelper.log("開(kāi)始查詢需要同步的用戶數(shù)據(jù)...");
// 模擬查詢到10條數(shù)據(jù)
int userCount = 10;
XxlJobHelper.log("查詢到{}條需要同步的用戶數(shù)據(jù)", userCount);
// 2. 同步用戶數(shù)據(jù)到目標(biāo)系統(tǒng)(這里模擬調(diào)用接口)
XxlJobHelper.log("開(kāi)始同步用戶數(shù)據(jù)...");
for (int i = 1; i <= userCount; i++) {
XxlJobHelper.log("正在同步第{}條用戶數(shù)據(jù)...", i);
// 模擬同步耗時(shí)
Thread.sleep(100);
}
// 3. 同步完成,更新同步狀態(tài)(這里模擬更新數(shù)據(jù)庫(kù))
XxlJobHelper.log("用戶數(shù)據(jù)同步完成,共同步{}條", userCount);
XxlJobHelper.handleSuccess("用戶數(shù)據(jù)同步成功,共同步" + userCount + "條");
} catch (Exception e) {
XxlJobHelper.log("用戶數(shù)據(jù)同步失?。簕}", e.getMessage());
XxlJobHelper.handleFail("用戶數(shù)據(jù)同步失?。? + e.getMessage());
}
}
}這里要注意:
- 任務(wù)方法的返回值可以是 void,也可以是 String,但推薦用 void,然后通過(guò)XxlJobHelper.handleSuccess()或XxlJobHelper.handleFail()來(lái)標(biāo)記任務(wù)狀態(tài);
- 日志一定要用XxlJobHelper.log(),這樣在調(diào)度中心的 “任務(wù)日志” 里才能看到,用自己項(xiàng)目的 logger 也能打印,但得去執(zhí)行器的日志文件里看,不方便;
- 要是任務(wù)執(zhí)行失敗,一定要拋異?;蛘哒{(diào)用XxlJobHelper.handleFail(),不然調(diào)度中心會(huì)認(rèn)為任務(wù)執(zhí)行成功,到時(shí)候出問(wèn)題了都不知道。
第五步:?jiǎn)?dòng)執(zhí)行器并驗(yàn)證注冊(cè)
寫(xiě)完代碼之后,啟動(dòng) Spring Boot 項(xiàng)目。啟動(dòng)成功之后,去調(diào)度中心的 “執(zhí)行器管理” 頁(yè)面,就能看到咱們配置的執(zhí)行器(xxl-job-executor-demo),而且 “在線機(jī)器地址” 里會(huì)顯示執(zhí)行器的 IP 和端口,狀態(tài)是 “在線”,這就說(shuō)明執(zhí)行器注冊(cè)成功了!
要是沒(méi)注冊(cè)上,別慌,先檢查這幾點(diǎn):
- 執(zhí)行器的 appname 是不是跟調(diào)度中心里的一致;
- 調(diào)度中心的 addresses 是不是寫(xiě)對(duì)了;
- 執(zhí)行器的端口是不是被占用了;
- 看執(zhí)行器的日志,有沒(méi)有報(bào) “注冊(cè)失敗” 的錯(cuò)誤,比如連接不上調(diào)度中心、令牌不一致等。
三、核心功能詳解:xxl-job 能幫咱們干哪些活?
部署好調(diào)度中心和執(zhí)行器之后,咱們就該用 xxl-job 來(lái)配置任務(wù)了。調(diào)度中心的 “任務(wù)管理” 頁(yè)面是核心,這里能配置任務(wù)、啟動(dòng) / 停止任務(wù)、查看日志,咱們一個(gè)個(gè)說(shuō)。
3.1 配置一個(gè)任務(wù)(以 printTimeJob 為例)
點(diǎn)擊 “任務(wù)管理”->“新增”,會(huì)彈出一個(gè)表單,里面的字段很多,但關(guān)鍵的就幾個(gè),咱們一個(gè)個(gè)填:
- 執(zhí)行器:選擇咱們之前注冊(cè)的執(zhí)行器(xxl-job-executor-demo);
- 任務(wù)描述:寫(xiě)清楚這個(gè)任務(wù)是干嘛的,比如 “每隔 5 秒打印當(dāng)前時(shí)間”,別寫(xiě) “任務(wù) 1”,不然過(guò)倆月你自己都忘了;
- 調(diào)度類型:選 “CRON”,這是最常用的,支持復(fù)雜的定時(shí)規(guī)則;
- CRON 表達(dá)式:比如 “*/5 * * * * ?”,表示每隔 5 秒執(zhí)行一次。要是不會(huì)寫(xiě) CRON 表達(dá)式,調(diào)度中心里有個(gè) “CRON 表達(dá)式生成器”,點(diǎn)進(jìn)去選就行,特別方便;
- 任務(wù)參數(shù):要是任務(wù)需要參數(shù),可以在這里填,比如同步用戶數(shù)據(jù)的任務(wù)需要填 “同步日期 = 2024-05-20”,后面在任務(wù)方法里可以通過(guò)XxlJobHelper.getJobParam()獲?。?/li>
- 執(zhí)行器路由策略:選 “第一個(gè)” 就行,后面再講其他策略;
- 調(diào)度過(guò)期策略:選 “忽略”,意思是要是任務(wù)因?yàn)槟撤N原因過(guò)期了(比如執(zhí)行器掛了,恢復(fù)后發(fā)現(xiàn)該執(zhí)行的任務(wù)沒(méi)執(zhí)行),就忽略,不補(bǔ)執(zhí)行;
- 阻塞處理策略:選 “單機(jī)串行”,意思是要是上一個(gè)任務(wù)還沒(méi)執(zhí)行完,下一個(gè)任務(wù)就等著,別并發(fā)執(zhí)行;
- 失敗重試次數(shù):選 1,意思是任務(wù)執(zhí)行失敗后,再重試 1 次;
- 任務(wù)超時(shí)時(shí)間:填 0,意思是不超時(shí),要是有超時(shí)需求可以填具體時(shí)間(毫秒),比如 300000 表示 5 分鐘超時(shí),超時(shí)后任務(wù)會(huì)被強(qiáng)制終止;
- GLUE 類型:選 “BEAN”,意思是任務(wù)邏輯在執(zhí)行器的 Java 代碼里(就是咱們寫(xiě)的@XxlJob方法),后面再講 “GLUE” 類型;
- GLUE 備注:不用填;
- 執(zhí)行參數(shù):不用填;
- Misfire 策略:選 “默認(rèn)” 就行。
填完之后點(diǎn) “保存”,然后在任務(wù)列表里找到這個(gè)任務(wù),點(diǎn)擊 “啟動(dòng)”,任務(wù)就開(kāi)始執(zhí)行了!
3.2 查看任務(wù)執(zhí)行情況和日志
任務(wù)啟動(dòng)后,怎么知道它有沒(méi)有在執(zhí)行呢?有兩個(gè)地方可以看:
- 任務(wù)列表的 “上次執(zhí)行時(shí)間” 和 “下次執(zhí)行時(shí)間”:要是 “上次執(zhí)行時(shí)間” 在不斷更新,說(shuō)明任務(wù)在正常執(zhí)行;
- “調(diào)度日志” 頁(yè)面:這里能看到每個(gè)任務(wù)的調(diào)度記錄,包括調(diào)度時(shí)間、調(diào)度結(jié)果、執(zhí)行器地址等。要是調(diào)度結(jié)果是 “成功”,說(shuō)明調(diào)度中心成功把任務(wù)指令發(fā)給了執(zhí)行器;要是 “失敗”,會(huì)顯示失敗原因,比如執(zhí)行器離線、路由策略找不到執(zhí)行器等。
要是想查看任務(wù)執(zhí)行的詳細(xì)日志,點(diǎn)擊任務(wù)列表里的 “日志” 按鈕,就能進(jìn)入 “任務(wù)日志” 頁(yè)面。這里能看到每次任務(wù)執(zhí)行的日志,包括XxlJobHelper.log()打印的內(nèi)容,還有執(zhí)行結(jié)果(成功 / 失?。?、執(zhí)行時(shí)間、耗時(shí)等。要是任務(wù)執(zhí)行失敗,日志里會(huì)顯示錯(cuò)誤信息,直接就能定位問(wèn)題,不用再去執(zhí)行器的服務(wù)器上翻日志了,這一點(diǎn)真的太香了!
3.3 常用的核心功能講解
除了上面說(shuō)的基本配置,xxl-job 還有很多實(shí)用的功能,咱們挑幾個(gè)常用的講講:
3.3.1 執(zhí)行器路由策略(集群環(huán)境下必用)
當(dāng)執(zhí)行器是集群(多個(gè)執(zhí)行器節(jié)點(diǎn))的時(shí)候,調(diào)度中心會(huì)根據(jù) “路由策略” 來(lái)決定把任務(wù)發(fā)給哪個(gè)執(zhí)行器。常用的路由策略有:
- 第一個(gè):總是把任務(wù)發(fā)給集群里的第一個(gè)執(zhí)行器;
- 最后一個(gè):總是把任務(wù)發(fā)給集群里的最后一個(gè)執(zhí)行器;
- 輪詢:按順序把任務(wù)發(fā)給每個(gè)執(zhí)行器,比如第一個(gè)任務(wù)發(fā)給節(jié)點(diǎn) 1,第二個(gè)發(fā)給節(jié)點(diǎn) 2,第三個(gè)再發(fā)給節(jié)點(diǎn) 1,適合負(fù)載均衡;
- 隨機(jī):隨機(jī)把任務(wù)發(fā)給某個(gè)執(zhí)行器,適合對(duì)執(zhí)行器沒(méi)有特殊要求的場(chǎng)景;
- 一致性 HASH:根據(jù)任務(wù)參數(shù)的 HASH 值來(lái)選擇執(zhí)行器,同一個(gè)參數(shù)的任務(wù)會(huì)發(fā)給同一個(gè)執(zhí)行器,適合需要任務(wù)冪等的場(chǎng)景,比如根據(jù)用戶 ID 同步數(shù)據(jù),同一個(gè)用戶的任務(wù)總是發(fā)給同一個(gè)執(zhí)行器,避免重復(fù)同步;
- 最不經(jīng)常使用:把任務(wù)發(fā)給最近執(zhí)行任務(wù)最少的執(zhí)行器,適合負(fù)載不均的場(chǎng)景;
- 最近最久未使用:把任務(wù)發(fā)給最近最久沒(méi)執(zhí)行任務(wù)的執(zhí)行器,也適合負(fù)載均衡;
- 故障轉(zhuǎn)移:先選一個(gè)執(zhí)行器發(fā)任務(wù),要是失敗了就換下一個(gè),直到成功,適合對(duì)任務(wù)執(zhí)行成功率要求高的場(chǎng)景;
- 忙碌轉(zhuǎn)移:先檢查執(zhí)行器的狀態(tài),要是執(zhí)行器忙碌(有任務(wù)在執(zhí)行),就換下一個(gè),適合不想讓執(zhí)行器過(guò)載的場(chǎng)景。
實(shí)際項(xiàng)目中,最常用的是 “輪詢” 和 “一致性 HASH”,根據(jù)業(yè)務(wù)場(chǎng)景選就行。比如咱們的同步用戶數(shù)據(jù)任務(wù),用 “輪詢” 就能讓多個(gè)執(zhí)行器分?jǐn)側(cè)蝿?wù),提高效率;要是任務(wù)需要根據(jù)參數(shù)冪等,就用 “一致性 HASH”。
3.3.2 GLUE 模式(改任務(wù)邏輯不用重新部署)
之前咱們配置任務(wù)的時(shí)候,GLUE 類型選的是 “BEAN”,意思是任務(wù)邏輯在執(zhí)行器的 Java 代碼里,要是改任務(wù)邏輯,就得改代碼、重新打包、部署執(zhí)行器,很麻煩。
而 “GLUE” 模式(支持 Java、Python、NodeJS、PHP、Shell)就不一樣了 —— 任務(wù)邏輯直接寫(xiě)在調(diào)度中心的網(wǎng)頁(yè)上,改完之后不用部署執(zhí)行器,直接生效!
比如咱們寫(xiě)一個(gè) Shell 腳本的 GLUE 任務(wù),步驟是:
- 新增任務(wù),GLUE 類型選 “GLUE (Shell)”;
- 點(diǎn)擊任務(wù)列表里的 “GLUE” 按鈕,進(jìn)入 GLUE 編輯頁(yè)面,寫(xiě) Shell 腳本,比如:
#!/bin/bash
echo "當(dāng)前時(shí)間:$(date +%Y-%m-%d\ %H:%M:%S)"
echo "這是一個(gè)GLUE Shell任務(wù)"- 保存腳本,啟動(dòng)任務(wù),然后查看日志,就能看到腳本執(zhí)行的結(jié)果。
要是想改腳本,直接在網(wǎng)頁(yè)上改,保存之后下一次任務(wù)執(zhí)行就會(huì)用新腳本,不用動(dòng)執(zhí)行器的代碼。這個(gè)模式特別適合經(jīng)常改邏輯的簡(jiǎn)單任務(wù),比如清理日志、備份數(shù)據(jù)這些,省了很多部署的時(shí)間。
不過(guò)要注意,GLUE 模式適合簡(jiǎn)單任務(wù),復(fù)雜的業(yè)務(wù)邏輯還是建議用 BEAN 模式,放在執(zhí)行器里寫(xiě),方便調(diào)試和維護(hù)。
3.3.3 失敗重試和報(bào)警(不用半夜盯任務(wù))
之前咱們配置任務(wù)的時(shí)候,填了 “失敗重試次數(shù)”,比如填 1,意思是任務(wù)執(zhí)行失敗后,調(diào)度中心會(huì)再重試 1 次。這個(gè)功能能避免很多偶發(fā)的失敗,比如網(wǎng)絡(luò)波動(dòng)、數(shù)據(jù)庫(kù)臨時(shí)不可用等,提高任務(wù)執(zhí)行的成功率。
要是重試之后還是失敗,怎么辦呢?xxl-job 支持報(bào)警功能,能把失敗信息發(fā)給指定的人,比如通過(guò)郵件、釘釘、企業(yè)微信等。
配置報(bào)警的步驟也很簡(jiǎn)單:
- 先在調(diào)度中心的 “系統(tǒng)管理”->“報(bào)警組管理” 里新建一個(gè)報(bào)警組,填報(bào)警組名稱,然后添加接收?qǐng)?bào)警的郵箱(支持多個(gè),用逗號(hào)分隔);
- 在 “任務(wù)管理” 里編輯任務(wù),把 “報(bào)警郵件” 選成咱們新建的報(bào)警組;
- 當(dāng)任務(wù)執(zhí)行失?。òㄖ卦嚭笫。{(diào)度中心就會(huì)給報(bào)警組里的郵箱發(fā)郵件,郵件里會(huì)包含任務(wù)名稱、執(zhí)行時(shí)間、失敗原因等信息。
要是想支持釘釘或企業(yè)微信報(bào)警,需要自己寫(xiě)報(bào)警處理器,實(shí)現(xiàn)com.xxl.job.core.alarm.JobAlarm接口,然后在調(diào)度中心的配置里注冊(cè)這個(gè)處理器,網(wǎng)上有很多現(xiàn)成的示例,照著改就行。
有了報(bào)警功能,再也不用半夜盯著任務(wù)了,要是任務(wù)失敗,郵件會(huì)直接發(fā)給你,第二天上班處理就行(前提是領(lǐng)導(dǎo)允許你第二天處理,哈哈)。
3.3.4 任務(wù)依賴(先執(zhí)行 A 任務(wù),再執(zhí)行 B 任務(wù))
有時(shí)候咱們的任務(wù)有依賴關(guān)系,比如必須先執(zhí)行 “同步用戶數(shù)據(jù)” 任務(wù)(A 任務(wù)),再執(zhí)行 “同步用戶訂單數(shù)據(jù)” 任務(wù)(B 任務(wù)),這時(shí)候就需要用 xxl-job 的 “任務(wù)依賴” 功能。
配置任務(wù)依賴的步驟:
- 先確保 A 任務(wù)已經(jīng)存在;
- 新增 B 任務(wù)的時(shí)候,在 “任務(wù)依賴” 字段里選擇 A 任務(wù);
- 保存之后,B 任務(wù)會(huì)在 A 任務(wù)執(zhí)行成功之后才會(huì)執(zhí)行;要是 A 任務(wù)執(zhí)行失敗,B 任務(wù)就不會(huì)執(zhí)行。
這個(gè)功能特別適合有先后順序的任務(wù),比如數(shù)據(jù)同步場(chǎng)景,必須先同步基礎(chǔ)數(shù)據(jù),再同步關(guān)聯(lián)數(shù)據(jù),不然會(huì)出現(xiàn)數(shù)據(jù)缺失的問(wèn)題。
四、進(jìn)階技巧:讓 xxl-job 用得更順手
咱們已經(jīng)會(huì)用 xxl-job 的基本功能了,但在實(shí)際項(xiàng)目中,還有一些進(jìn)階技巧能讓 xxl-job 用得更順手,比如監(jiān)控、集群部署、性能優(yōu)化等。
4.1 監(jiān)控報(bào)表(一眼看清任務(wù)狀態(tài))
調(diào)度中心的 “監(jiān)控報(bào)表” 頁(yè)面特別實(shí)用,能看到任務(wù)的執(zhí)行情況、失敗統(tǒng)計(jì)、執(zhí)行器狀態(tài)等,主要有三個(gè)報(bào)表:
- 任務(wù)執(zhí)行報(bào)表:按天統(tǒng)計(jì)任務(wù)的執(zhí)行次數(shù)、成功次數(shù)、失敗次數(shù)、平均耗時(shí),能直觀看到任務(wù)的執(zhí)行趨勢(shì);
- 任務(wù)失敗報(bào)表:統(tǒng)計(jì)最近失敗的任務(wù),包括任務(wù)名稱、失敗時(shí)間、失敗原因,能快速定位問(wèn)題任務(wù);
- 執(zhí)行器報(bào)表:統(tǒng)計(jì)每個(gè)執(zhí)行器的在線狀態(tài)、任務(wù)執(zhí)行次數(shù)、平均耗時(shí),能看到執(zhí)行器的負(fù)載情況。
每天上班先看一眼監(jiān)控報(bào)表,就能知道昨天的任務(wù)有沒(méi)有問(wèn)題,執(zhí)行器是不是正常,不用一個(gè)個(gè)去查任務(wù)日志,省了很多時(shí)間。
4.2 調(diào)度中心集群(保證高可用)
調(diào)度中心是 xxl-job 的核心,要是調(diào)度中心掛了,所有任務(wù)都沒(méi)法執(zhí)行,所以必須做集群部署,保證高可用。
調(diào)度中心集群部署特別簡(jiǎn)單,步驟是:
- 準(zhǔn)備兩臺(tái)(或多臺(tái))服務(wù)器,都部署調(diào)度中心(步驟跟之前一樣);
- 所有調(diào)度中心都連接同一個(gè) MySQL 數(shù)據(jù)庫(kù)(因?yàn)槿蝿?wù)配置、調(diào)度記錄都存在數(shù)據(jù)庫(kù)里,必須共享);
- 在前面加一個(gè) Nginx,做負(fù)載均衡,把請(qǐng)求分發(fā)到各個(gè)調(diào)度中心節(jié)點(diǎn)。
Nginx 的配置示例:
upstream xxl-job-admin {
server 192.168.1.100:8080; # 調(diào)度中心節(jié)點(diǎn)1
server 192.168.1.101:8080; # 調(diào)度中心節(jié)點(diǎn)2
}
server {
listen 80;
server_name xxl-job-admin.example.com; # 自己的域名
location / {
proxy_pass http://xxl-job-admin;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}這樣一來(lái),就算其中一個(gè)調(diào)度中心節(jié)點(diǎn)掛了,另一個(gè)還能正常工作,任務(wù)調(diào)度不會(huì)受影響。而且執(zhí)行器配置調(diào)度中心地址的時(shí)候,直接填 Nginx 的地址(比如http://xxl-job-admin.example.com/xxl-job-admin)就行,不用填所有節(jié)點(diǎn)的地址。
4.3 執(zhí)行器集群(提高任務(wù)執(zhí)行效率)
當(dāng)任務(wù)量很大的時(shí)候,單個(gè)執(zhí)行器可能處理不過(guò)來(lái),這時(shí)候就需要部署執(zhí)行器集群,多個(gè)執(zhí)行器一起干活,提高效率。
執(zhí)行器集群部署也很簡(jiǎn)單:
- 準(zhǔn)備兩臺(tái)(或多臺(tái))服務(wù)器,都部署同一個(gè)執(zhí)行器(代碼一樣);
- 所有執(zhí)行器的 appname 必須一致(不然調(diào)度中心會(huì)認(rèn)為是不同的執(zhí)行器);
- 所有執(zhí)行器的 port 不一樣(比如節(jié)點(diǎn) 1 是 9999,節(jié)點(diǎn) 2 是 10000);
- 所有執(zhí)行器的 admin.addresses 都填調(diào)度中心的地址(要是調(diào)度中心是集群,就填 Nginx 的地址)。
部署完成之后,去調(diào)度中心的 “執(zhí)行器管理” 頁(yè)面,就能看到 “在線機(jī)器地址” 里有多個(gè)執(zhí)行器節(jié)點(diǎn),然后配置任務(wù)的時(shí)候,路由策略選 “輪詢”,就能讓多個(gè)執(zhí)行器分?jǐn)側(cè)蝿?wù)了。
比如咱們的同步用戶數(shù)據(jù)任務(wù),之前單個(gè)執(zhí)行器同步 1000 條數(shù)據(jù)要 10 分鐘,部署兩個(gè)執(zhí)行器之后,每個(gè)執(zhí)行器同步 500 條,5 分鐘就能完成,效率直接翻倍。
4.4 性能優(yōu)化(讓 xxl-job 跑得更快)
在高并發(fā)場(chǎng)景下,比如有上千個(gè)任務(wù)同時(shí)執(zhí)行,xxl-job 可能會(huì)出現(xiàn)調(diào)度延遲、執(zhí)行慢的問(wèn)題,這時(shí)候就需要做一些性能優(yōu)化:
- 優(yōu)化數(shù)據(jù)庫(kù):調(diào)度中心的數(shù)據(jù)庫(kù)是瓶頸之一,建議給常用的表(比如 xxl_job_qrtz_trigger_log、xxl_job_qrtz_fired_trigger)加索引,比如給 trigger_log 表的 job_id、trigger_time 字段加索引;另外,定期清理歷史日志,比如只保留 30 天的日志,避免表數(shù)據(jù)量太大導(dǎo)致查詢慢;
- 調(diào)整線程池參數(shù):調(diào)度中心和執(zhí)行器都有線程池,默認(rèn)參數(shù)可能不夠用,可以根據(jù)實(shí)際情況調(diào)整。比如調(diào)度中心的xxl.job.admin.triggerpool.fast.max和xxl.job.admin.triggerpool.slow.max,分別表示快速任務(wù)和慢速任務(wù)的線程池大小,要是任務(wù)多,可以調(diào)大;執(zhí)行器的線程池大小可以在配置類里設(shè)置,比如xxlJobSpringExecutor.setExecutorPoolCoreSize(10);
- 避免任務(wù)阻塞:要是任務(wù)執(zhí)行時(shí)間太長(zhǎng)(比如超過(guò) 1 小時(shí)),會(huì)占用執(zhí)行器的線程,導(dǎo)致其他任務(wù)阻塞,建議把長(zhǎng)任務(wù)拆成短任務(wù),比如把 “同步一天的用戶數(shù)據(jù)” 拆成 “同步每小時(shí)的用戶數(shù)據(jù)”,每個(gè)任務(wù)執(zhí)行時(shí)間短,不容易阻塞;
- 使用異步執(zhí)行:要是任務(wù)不需要同步執(zhí)行(比如發(fā)送通知、日志統(tǒng)計(jì)),可以在任務(wù)方法里用異步線程執(zhí)行,比如用 Spring 的@Async注解,這樣任務(wù)方法能快速返回,釋放執(zhí)行器的線程,提高并發(fā)能力。
五、實(shí)際項(xiàng)目中的最佳實(shí)踐和踩坑經(jīng)驗(yàn)
最后,跟大家分享一些實(shí)際項(xiàng)目中用 xxl-job 的最佳實(shí)踐和踩坑經(jīng)驗(yàn),都是我踩過(guò)的坑,希望大家能避開(kāi)。
5.1 最佳實(shí)踐
- 任務(wù)命名規(guī)范:任務(wù)名稱要清晰,能看出來(lái)是干嘛的,比如 “sync_user_data_daily”(每天同步用戶數(shù)據(jù)),別用 “task1”“job2” 這種模糊的名稱,不然時(shí)間長(zhǎng)了沒(méi)人知道是干嘛的;
- 參數(shù)配置化:任務(wù)需要的參數(shù)(比如同步日期、同步數(shù)量)別硬編碼在代碼里,要么在任務(wù)配置的 “任務(wù)參數(shù)” 里填,要么存在配置中心(比如 Nacos、Apollo),這樣改參數(shù)不用改代碼;
- 任務(wù)冪等性:分布式任務(wù)一定要保證冪等性,比如同步用戶數(shù)據(jù)的時(shí)候,先查一下數(shù)據(jù)有沒(méi)有已經(jīng)同步過(guò),避免重復(fù)同步導(dǎo)致數(shù)據(jù)重復(fù)??梢杂萌蝿?wù)參數(shù)(比如同步日期 + 用戶 ID)作為唯一鍵,或者在數(shù)據(jù)庫(kù)里加唯一索引;
- 任務(wù)拆分:別把所有邏輯都放在一個(gè)任務(wù)里,比如 “同步用戶數(shù)據(jù) + 同步訂單數(shù)據(jù) + 發(fā)送通知”,應(yīng)該拆成三個(gè)獨(dú)立的任務(wù),這樣某個(gè)任務(wù)失敗了,不會(huì)影響其他任務(wù),而且排查問(wèn)題也方便;
- 定期檢查任務(wù):每周或每月檢查一下任務(wù)的執(zhí)行情況,看看有沒(méi)有長(zhǎng)期失敗的任務(wù)、執(zhí)行時(shí)間過(guò)長(zhǎng)的任務(wù),及時(shí)優(yōu)化,避免問(wèn)題積累。
5.2 踩坑經(jīng)驗(yàn)
- 調(diào)度中心和執(zhí)行器時(shí)間不同步:之前遇到過(guò)任務(wù)到點(diǎn)不執(zhí)行的問(wèn)題,查了半天發(fā)現(xiàn)執(zhí)行器服務(wù)器的時(shí)間比調(diào)度中心慢了 10 分鐘,調(diào)度中心認(rèn)為任務(wù)該執(zhí)行了,但執(zhí)行器的時(shí)間還沒(méi)到,所以不執(zhí)行。解決方法:所有服務(wù)器(包括調(diào)度中心和執(zhí)行器)都配置 NTP 時(shí)間同步,保證時(shí)間一致;
- 任務(wù)參數(shù)有特殊字符:要是任務(wù)參數(shù)里有逗號(hào)、空格等特殊字符,在 BEAN 模式下用XxlJobHelper.getJobParam()獲取的時(shí)候會(huì)有問(wèn)題,比如參數(shù)是 “startDate=2024-05-20,endDate=2024-05-21”,逗號(hào)會(huì)被當(dāng)成參數(shù)分隔符。解決方法:把參數(shù)用 JSON 格式傳遞,比如 “{"startDate":"2024-05-20","endDate":"2024-05-21"}”,然后在代碼里解析 JSON;
- 執(zhí)行器日志路徑不存在:要是執(zhí)行器配置的 logpath 路徑不存在,執(zhí)行器會(huì)啟動(dòng)失敗。解決方法:?jiǎn)?dòng)執(zhí)行器之前,先創(chuàng)建好日志路徑,或者在配置里用默認(rèn)路徑;
- CRON 表達(dá)式寫(xiě)錯(cuò):比如想寫(xiě) “每天凌晨 2 點(diǎn)執(zhí)行”,寫(xiě)成了 “0 0 2 * * ?” 是對(duì)的,但寫(xiě)成 “0 2 0 * * ?” 就錯(cuò)了,會(huì)變成每天凌晨 0 點(diǎn) 2 分執(zhí)行。解決方法:用調(diào)度中心的 “CRON 表達(dá)式生成器” 生成 CRON,生成之后先測(cè)試一下,比如把 CRON 改成 “*/1 * * * * ?”(每隔 1 秒執(zhí)行),看任務(wù)是不是正常執(zhí)行,沒(méi)問(wèn)題再改成實(shí)際的 CRON;
- 調(diào)度中心集群沒(méi)共享數(shù)據(jù)庫(kù):之前部署調(diào)度中心集群的時(shí)候,兩個(gè)節(jié)點(diǎn)連接了不同的 MySQL 數(shù)據(jù)庫(kù),導(dǎo)致任務(wù)配置不同步,一個(gè)節(jié)點(diǎn)能看到任務(wù),另一個(gè)節(jié)點(diǎn)看不到。解決方法:所有調(diào)度中心節(jié)點(diǎn)必須連接同一個(gè) MySQL 數(shù)據(jù)庫(kù),保證數(shù)據(jù)共享。
六、總結(jié)
不知不覺(jué)寫(xiě)了這么多,從 xxl-job 的基礎(chǔ)介紹,到實(shí)戰(zhàn)部署、核心功能、進(jìn)階技巧,再到最佳實(shí)踐和踩坑經(jīng)驗(yàn),應(yīng)該把 xxl-job 的核心內(nèi)容都覆蓋到了。
說(shuō)實(shí)話,xxl-job 真的是一個(gè)特別優(yōu)秀的分布式任務(wù)調(diào)度框架,上手簡(jiǎn)單,功能強(qiáng)大,穩(wěn)定性好,而且開(kāi)源免費(fèi),國(guó)內(nèi)很多大公司都在使用,不用擔(dān)心后續(xù)維護(hù)問(wèn)題。


































