定時任務是大家在開發(fā)中一個不可避免的業(yè)務,比如在一些電商系統(tǒng)中可能會定時給用戶發(fā)送生日券,一些對賬系統(tǒng)中可能會定時去對賬。如果服務只有一臺機子,那直接用ScheduledExecutorService就可以了,但是現(xiàn)在服務一般是集群部署,這個時候,ScheduledExecutorService很多時候就沒法解決問題,因為我們需要保證只執(zhí)行一次,同時,我們還需要保證高可用,任務監(jiān)控等等。
因此,鑒于這些場景,出現(xiàn)了較多的分布式調(diào)度框架,本文將對也就主流的開源調(diào)度器進行分析對比,然后總結出分布式調(diào)度器的系統(tǒng)設計關鍵點。
需求分析
設計一個分布式調(diào)度器,首先需要明確面臨哪些場景,以及需要解決哪些問題。按照需求特點,我們將其分為功能性需求和非功能性需求。
功能性需求

功能性需求,主要包括:
- 任務觸發(fā)調(diào)度功能:支持單機調(diào)度、支持集群調(diào)度、支持精確觸發(fā)實踐調(diào)度、支持實踐表達式實踐調(diào)度、支持API調(diào)度
- 任務編排功能:支持JOB依賴關系設置、支持JOB級聯(lián)關系觸發(fā)
- 支持大任務拆分和并行處理功能:支持將大任務拆分成多級子任務、支持應用集群并行執(zhí)行子任務。
- 任務可視化管理
非功能性需求

對于調(diào)度器,一般需要滿足:高可用、高性能、支持水平處理等能力,我們將這些能力成為非功能性需求。
開源框架分析
Java Timer
在ScheduledExecutorService未出現(xiàn)前,在業(yè)務開發(fā)中如果遇到執(zhí)行一些簡單定時任務的需求,為了避免做一些看起來復雜的控制邏輯,一般考慮使用 Timer 來實現(xiàn)定時任務的執(zhí)行,下面給出一個最簡單用法的例子:
Timer timer = new Timer();
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
// scheduledExecutionTime() 返回此任務最近開始執(zhí)行的時間
Date date = new Date(this.scheduledExecutionTime());
System.out.println("timeTask run " + date);
}
};
// 從現(xiàn)在開始每間隔 1000 ms 計劃執(zhí)行一個任務
timer.schedule(timerTask, 0, 1000);
Timer現(xiàn)在已經(jīng)不建議使用了,推薦使用ScheduledExecutorService,為什么不建議使用Timer了?原因如下:
- Timer底層是使用單線程來處理多個Timer任務,這意味著所有任務實際上都是串行執(zhí)行,前一個任務的延遲會影響到之后的任務的執(zhí)行;
- 由于單線程的緣故,一旦某個定時任務在運行時,產(chǎn)生未處理的異常,那么不僅當前這個線程會停止,所有的定時任務都會停止;
- Timer任務執(zhí)行是依賴于系統(tǒng)絕對時間,系統(tǒng)時間變化會導致執(zhí)行計劃的變更。
由于上述缺陷,盡量不要使用Timer, idea中也會明確提示,使用ScheduledThreadPoolExecutor替代Timer 。
Java ScheduledExecutorService
ScheduledExecutorService對于Timer的缺陷進行了修補,首先ScheduledExecutorService內(nèi)部實現(xiàn)是ScheduledThreadPool線程池,可以支持多個任務并發(fā)執(zhí)行。
對于某一個線程執(zhí)行的任務出現(xiàn)異常,也會處理,不會影響其他線程任務的執(zhí)行,另外ScheduledExecutorService是基于時間間隔的延遲,執(zhí)行不會由于系統(tǒng)時間的改變發(fā)生變化。
當然,ScheduledExecutorService也有自己的局限性:只能根據(jù)任務的延遲來進行調(diào)度,無法滿足基于絕對時間和日歷調(diào)度的需求。
Spring Task
spring task 是spring自主開發(fā)的輕量級定時任務框架,不需要依賴其他額外的包,配置較為簡單,此處使用注解配置:

Spring Task 本身不支持持久化,也沒有推出官方的分布式集群模式,只能靠開發(fā)者在業(yè)務應用中自己手動擴展實現(xiàn),無法滿足可視化,易配置的需求。
Quartz
Quartz框架是Java領域最著名的開源任務調(diào)度工具,也是目前事實上的定時任務標準,幾乎全部的開源定時任務框架都是基于Quartz核心調(diào)度構建而成。

關鍵概念
- Scheduler:任務調(diào)度器,是執(zhí)行任務調(diào)度的控制器。本質(zhì)上是一個計劃調(diào)度容器,注冊了全部Trigger和對應的JobDetail, 使用線程池作為任務運行的基礎組件,提高任務執(zhí)行效率。
- Trigger:觸發(fā)器,用于定義任務調(diào)度的時間規(guī)則,告訴任務調(diào)度器什么時候觸發(fā)任務,其中CronTrigger是基于cron表達式構建的功能強大的觸發(fā)器。
- Calendar:日歷特定時間點的集合。一個trigger可以包含多個Calendar,可用于排除或包含某些時間點。
- JobDetail:是一個可執(zhí)行的工作,用來描述Job實現(xiàn)類及其它相關的靜態(tài)信息,如Job的名稱、監(jiān)聽器等相關信息。
- Job:任務執(zhí)行接口,只有一個execute方法,用于執(zhí)行真正的業(yè)務邏輯。
- JobStore:任務存儲方式,主要有RAMJobStore和JDBCJobStore,RAMJobStore是存儲在JVM的內(nèi)存中,有丟失和數(shù)量受限的風險,JDBCJobStore是將任務信息持久化到數(shù)據(jù)庫中,支持集群。
實現(xiàn)細節(jié)
1.Quartz如何保證任務只在一個節(jié)點運行?
答案是使用了數(shù)據(jù)庫鎖。在quartz的集群解決方案里有張表scheduler_locks,quartz采用了悲觀鎖的方式對triggers表進行行加鎖,以保證任務同步的正確性。一旦某一個節(jié)點上面的線程獲取了該鎖,那么這個Job就會在這臺機器上被執(zhí)行,同時這個鎖就會被這臺機器占用。同時另外一臺機器也會想要觸發(fā)這個任務,但是鎖已經(jīng)被占用了,就只能等待,直到這個鎖被釋放。之后會看trigger狀態(tài),如果已經(jīng)被執(zhí)行了,則不會執(zhí)行了。
簡單地說,quartz的分布式調(diào)度策略是以數(shù)據(jù)庫為邊界資源的一種異步策略。各個調(diào)度器都遵守一個基于數(shù)據(jù)庫鎖的操作規(guī)則從而保證了操作的唯一性。同時多個節(jié)點的異步運行保證了服務的可靠。但這種策略有自己的局限性:集群特性對于高CPU使用率的任務效果很好,但是對于大量的短任務,各個節(jié)點都會搶占數(shù)據(jù)庫鎖,這樣就出現(xiàn)大量的線程等待資源。這種情況隨著節(jié)點的增加會越來越嚴重。
2.在集群的哪個節(jié)點上運行,Quartz是如何進行選取的?
隨緣的。集群中各個節(jié)點做到時間同步很重要。如果待觸發(fā)的任務少且運行快,那么很可能一直在時間最早的那一個節(jié)點上執(zhí)行。
3.任務太多,而線程池中配置的線程太少時怎么辦?
沒事,反正選取Trigger的時候也會考慮空閑線程的數(shù)量,空閑線程少的話實例就少選取幾個Trigger來執(zhí)行。
Quartz不足
Quartz通過故障轉(zhuǎn)移和負載均衡實現(xiàn)了任務的高可用,通過數(shù)據(jù)庫的鎖機制來確保任務執(zhí)行的唯一性,但是集群特性僅僅只是用來HA,節(jié)點數(shù)量的增加并不會提升單個任務的執(zhí)行效率,不能實現(xiàn)水平擴展。
總結起來,其缺陷和不足在于:
1)需要把任務信息持久化到業(yè)務數(shù)據(jù)表,和業(yè)務有耦合;
2)調(diào)度邏輯和執(zhí)行并存于同一個項目中,在機器性能固定的情況下,業(yè)務和調(diào)度之間不可避免的會相互影響;
3)quartz集群模式下,是通過數(shù)據(jù)庫獨占鎖來唯一獲取任務,任務執(zhí)行并沒有實現(xiàn)完善的負載均衡;
輕量級神器 XXL-JOB
XXL-JOB是一個輕量級分布式任務調(diào)度平臺,主打特點是平臺化,易部署,開發(fā)迅速、學習簡單、輕量級、易擴展,代碼仍在持續(xù)更新中。
“調(diào)度中心”是任務調(diào)度控制臺,平臺自身并不承擔業(yè)務邏輯,只是負責任務的統(tǒng)一管理和調(diào)度執(zhí)行,并且提供任務管理平臺, “執(zhí)行器” 負責接收“調(diào)度中心”的調(diào)度并執(zhí)行,可直接部署執(zhí)行器,也可以將執(zhí)行器集成到現(xiàn)有業(yè)務項目中。 通過將任務的調(diào)度控制和任務的執(zhí)行解耦,業(yè)務使用只需要關注業(yè)務邏輯的開發(fā)。
主要提供了任務的動態(tài)配置管理、任務監(jiān)控和統(tǒng)計報表以及調(diào)度日志幾大功能模塊,支持多種運行模式和路由策略,可基于對應執(zhí)行器機器集群數(shù)量進行簡單分片數(shù)據(jù)處理。
功能特性
主要功能特性如下:
簡單靈活
提供Web頁面對任務進行管理,管理系統(tǒng)支持用戶管理、權限控制; 支持容器部署;支持通過通用HTTP提供跨平臺任務調(diào)度;豐富的任務管理功能
支持頁面對任務CRUD操作; 支持在頁面編寫腳本任務、命令行任務、Java代碼任務并執(zhí)行; 支持任務級聯(lián)編排,父任務執(zhí)行結束后觸發(fā)子任務執(zhí)行; 支持設置任務優(yōu)先級; 支持設置指定任務執(zhí)行節(jié)點路由策略,包括輪詢、隨機、廣播、故障轉(zhuǎn)移、忙碌轉(zhuǎn)移等; 支持Cron方式、任務依賴、調(diào)度中心API接口方式觸發(fā)任務執(zhí)行高性能
調(diào)度中心基于線程池多線程觸發(fā)調(diào)度任務,快任務、慢任務基于線程池隔離調(diào)度,提供系統(tǒng)性能和穩(wěn)定性; 任務調(diào)度流程全異步化設計實現(xiàn),如異步調(diào)度、異步運行、異步回調(diào)等,有效對密集調(diào)度進行流量削峰;高可用
任務調(diào)度中心、任務執(zhí)行節(jié)點均 集群部署,支持動態(tài)擴展、故障轉(zhuǎn)移 支持任務配置路由故障轉(zhuǎn)移策略,執(zhí)行器節(jié)點不可用是自動轉(zhuǎn)移到其他節(jié)點執(zhí)行 支持任務超時控制、失敗重試配置 支持任務處理阻塞策略:調(diào)度當任務執(zhí)行節(jié)點忙碌時來不及執(zhí)行任務的處理策略,包括:串行、拋棄、覆蓋策略;易于監(jiān)控運維
支持設置任務失敗郵件告警,預留接口支持短信、釘釘告警; 支持實時查看任務執(zhí)行運行數(shù)據(jù)統(tǒng)計圖表、任務進度監(jiān)控數(shù)據(jù)、任務完整執(zhí)行日志;
系統(tǒng)設計
設計思路
將調(diào)度行為抽象形成“調(diào)度中心”公共平臺,而平臺自身并不承擔業(yè)務邏輯,“調(diào)度中心”負責發(fā)起調(diào)度請求;將任務抽象成分散的JobHandler,交由“執(zhí)行器”統(tǒng)一管理,“執(zhí)行器”負責接收調(diào)度請求并執(zhí)行對應的JobHandler中業(yè)務邏輯;因此,“調(diào)度”和“任務”兩部分可以相互解耦,提高系統(tǒng)整體穩(wěn)定性和擴展性;

系統(tǒng)組成
調(diào)度模塊(調(diào)度中心):負責管理調(diào)度信息,按照調(diào)度配置發(fā)出調(diào)度請求,自身不承擔業(yè)務代碼。調(diào)度系統(tǒng)與任務解耦,提高了系統(tǒng)可用性和穩(wěn)定性,同時調(diào)度系統(tǒng)性能不再受限于任務模塊;支持可視化、簡單且動態(tài)的管理調(diào)度信息,包括任務新建,更新,刪除,任務報警等,所有上述操作都會實時生效,同時支持監(jiān)控調(diào)度結果以及執(zhí)行日志,支持執(zhí)行器Failover
執(zhí)行模塊(執(zhí)行器):負責接收調(diào)度請求并執(zhí)行任務邏輯。任務模塊專注于任務的執(zhí)行等操作,開發(fā)和維護更加簡單和高效;接收“調(diào)度中心”的執(zhí)行請求、終止請求和日志請求等
系統(tǒng)HA設計
1、調(diào)度中心高可用
調(diào)度中心支持多節(jié)點部署,基于數(shù)據(jù)庫行鎖保證同時只有一個調(diào)度中心節(jié)點觸發(fā)任務調(diào)度,參考com.xxl.job.admin.core.thread.JobScheduleHelper#start
2、任務調(diào)度高可用
路由策略
調(diào)度中心基于路由策略路由選擇一個執(zhí)行器節(jié)點執(zhí)行任務,XXL-JOB提供了如下路由策略保證任務調(diào)度高可用;忙碌轉(zhuǎn)移策略
下發(fā)任務前向執(zhí)行器節(jié)點發(fā)起rpc心跳請求查詢是否忙碌,如果執(zhí)行器節(jié)點返回忙碌則轉(zhuǎn)移到其他執(zhí)行器節(jié)點執(zhí)行(參考 com.xxl.job.admin.core.route.strategy.ExecutorRouteBusyover);阻塞處理策略
當執(zhí)行器節(jié)點存在多個相同任務id的任務未執(zhí)行完成,則需要基于阻塞策略對任務進行取舍:
串行策略:默認策略,任務進行排隊、丟棄舊任務策略、丟棄新任務策略(參考:com.xxl.job.core.biz.impl.ExecutorBizImpl#run)故障轉(zhuǎn)移策略
下發(fā)任務前向執(zhí)行器節(jié)點發(fā)起rpc心跳請求查詢是否在線,如果執(zhí)行器節(jié)點沒返回或者返回不可用則轉(zhuǎn)移到其他執(zhí)行器節(jié)點執(zhí)行 (參考com.xxl.job.admin.core.route.strategy.ExecutorRouteFailover)
工作原理
XXL-JOB的工作原理如下如所示:

Elastic-Job
Elastic-Job是一個分布式調(diào)度解決方案,由兩個相互獨立的子項目Elastic-Job-Lite和Elastic-Job-Cloud組成。Elastic-Job-Lite定位為輕量級無中心化解決方案,使用jar包的形式提供分布式任務的協(xié)調(diào)服務。Elastic-Job-Cloud使用Mesos + Docker的解決方案,額外提供資源治理、應用分發(fā)以及進程隔離等服務。
原理解析

Saturn
Saturn是唯品會開源的一個分布式任務調(diào)度平臺,在Elastic Job的基礎上進行了改造。
SIA-TASK
是宜信開源的分布式任務調(diào)度平臺。
分布式調(diào)度關鍵點

分布式調(diào)度一般分為三部分,分別是:任務調(diào)度器、任務執(zhí)行器、任務。詳細如下:
- 任務調(diào)度器不關心業(yè)務邏輯,只關心任務的觸發(fā)策略、失敗策略、路由策略、阻塞處理策略
- 任務執(zhí)行器只需要監(jiān)聽任務觸發(fā)接口,按要求執(zhí)行任務,成功或失敗時異步通知任務調(diào)度器
- 任務的基本屬性需要包含任務ID、觸發(fā)策略、失敗策略、路由策略、阻塞處理策略、創(chuàng)建時間、創(chuàng)建用戶、任務參數(shù)、任務當前搶占調(diào)度器、任務狀態(tài)(調(diào)度中、執(zhí)行中、執(zhí)行完成)
高可用
為了避免單點故障,任務調(diào)度系統(tǒng)通常需要通過集群實現(xiàn)系統(tǒng)高可用,同時通過擴展提高系統(tǒng)的任務負載量上限。
鑒于任務調(diào)度系統(tǒng)的特殊性,“調(diào)度”和“執(zhí)行”兩個模塊需要均支持集群部署,由于職責不同,因此各自集群側(cè)重點也有有所不同。
“調(diào)度器”集群,目標為避免調(diào)度模塊單點故障。同時,集群節(jié)點需要通過鎖或命名服務保證單個任務的單次觸發(fā),只在其中一個節(jié)點上生效,以防止任務的重復觸發(fā)。
調(diào)度器為何采用集群而不是主從?
主從模式只能提供HA的特性,不能提高提高系統(tǒng)的任務負載量上限。
主從需要實現(xiàn)選舉算法,保證CP或者AP
集群只需要將狀態(tài)都遷移到全局的存儲器中(例如DB),任務可以采用搶占機制(全局鎖),搶占后在任務后標識任務狀態(tài)即可。
“執(zhí)行器”集群:目標為避免任務模塊單點故障。進一步可以通過自定義路由策略實現(xiàn)Failover等高級功能,從而在執(zhí)行器某臺機器節(jié)點故障時自動轉(zhuǎn)移不會影響到任務的正常觸發(fā)執(zhí)行。
失敗處理
這里的失敗策略指的是業(yè)務發(fā)生失敗的處理策略,而不是因為節(jié)點故障導致任務沒有執(zhí)行完成導致的失敗
任務失敗是一種很常見的情況,當任務失敗時有兩點非常重要,一個是快速發(fā)現(xiàn)問題,另一個是及時解決問題。
任務業(yè)務邏輯千差萬別,如索引同步、pv統(tǒng)計、訂單超時處理等等。任務失敗可能會導致非常嚴重的后果,比如索引同步任務失敗可能導致搜索不匹配,pv統(tǒng)計失敗可能導致打點報表的生成,訂單超時處理任務的失敗可能導致商品庫存的大量無效占用等等。
針對上述情況,通常有幾種處理方案:
- 失敗告警(快速發(fā)現(xiàn)問題):任務失敗時,主動向任務負責人發(fā)送告警通知,如郵件、短信等方式。這是一種常用的處理方案,原理和實現(xiàn)都比較簡單。負責人接收的告警郵件時,通過人工的方式進行故障處理,如手動觸發(fā)一次任務執(zhí)行。
- 失敗重試(快速解決問題):任務失敗時,調(diào)度中心主動嘗試觸發(fā)一次重試任務。優(yōu)點在于不需要人為接入,重試在一定程度上可以大大提高任務的成功率。但是,失敗重試需要注意限制重試次數(shù),否則將會導致”失敗-重試-失敗”的死循環(huán),造成資源浪費。
路由策略
由于任務執(zhí)行器存在多個實例,調(diào)度器如何選擇任務執(zhí)行器同樣是個問題。為每個任務配置不同的路由策略(為配置則使用默認路由策略)常見的策略如下:
- 隨機策略
- 輪訓策略
- 任務IDHash策略
- 背壓策略(需要調(diào)度器與執(zhí)行器通訊報告執(zhí)行器未執(zhí)行任務數(shù)量)
- 阻塞處理策略
阻塞策略
在調(diào)度比較密集,而執(zhí)行器來不及處理的情況下,任務阻塞策略可以指導執(zhí)行器快速處理阻塞的觸發(fā)請求。常見的阻塞策略有以下幾種:
- 單機串行(默認):調(diào)度請求進入執(zhí)行器后,調(diào)度請求進入FIFO隊列并以串行方式運行;
- 丟棄后續(xù)調(diào)度:調(diào)度請求進入執(zhí)行器后,發(fā)現(xiàn)執(zhí)行器存在運行的調(diào)度任務,本次請求將會被丟棄并標記為失??;
- 覆蓋之前調(diào)度:調(diào)度請求進入執(zhí)行器后,發(fā)現(xiàn)執(zhí)行器存在運行的調(diào)度任務,將會終止運行中的調(diào)度任務并清空隊列,然后運行本地調(diào)度任務;
分布式鎖實現(xiàn)
對于分布式定時任務系統(tǒng)來說,最重要的是分布式鎖,實現(xiàn)方式有三種:
- 基于數(shù)據(jù)庫的實現(xiàn)方式:唯一索引原理,比如一個定時任務一天執(zhí)行一次,我們就以日期作為唯一索引,誰第一個把當天日期插入成功,誰就有資格執(zhí)行;
- 基于Redis的實現(xiàn)方式
- 基于zk的實現(xiàn)方式