來自公眾號:JavaGuide
最近有朋友問到定時任務相關的問題。
于是,我簡單寫了一篇文章總結一下定時任務的一些概念以及一些常見的定時任務技術選型。希望能對小伙伴們有幫助!
個人能力有限。如果文章有任何需要補充/完善/修改的地方,歡迎在評論區(qū)指出,共同進步!
我們來看一下幾個非常常見的業(yè)務場景:
這些場景往往都要求我們在某個特定的時間去做某個事情。
java.util.Timer
是 JDK 1.3 開始就已經支持的一種定時任務的實現(xiàn)方式。
Timer
內部使用一個叫做 TaskQueue
的類存放定時任務,它是一個基于最小堆實現(xiàn)的優(yōu)先級隊列。TaskQueue
會按照任務距離下一次執(zhí)行時間的大小將任務排序,保證在堆頂?shù)娜蝿兆钕葓?zhí)行。這樣在需要執(zhí)行任務時,每次只需要取出堆頂?shù)娜蝿者\行即可!
Timer
使用起來比較簡單,通過下面的方式我們就能創(chuàng)建一個 1s 之后執(zhí)行的定時任務。
// 示例代碼:
TimerTask task = new TimerTask() {
public void run() {
System.out.println('當前時間: ' + new Date() + 'n' +
'線程名稱: ' + Thread.currentThread().getName());
}
};
System.out.println('當前時間: ' + new Date() + 'n' +
'線程名稱: ' + Thread.currentThread().getName());
Timer timer = new Timer('Timer');
long delay = 1000L;
timer.schedule(task, delay);
//輸出:
當前時間: Fri May 28 15:18:47 CST 2021n線程名稱: main
當前時間: Fri May 28 15:18:48 CST 2021n線程名稱: Timer
不過其缺陷較多,比如一個 Timer
一個線程,這就導致 Timer
的任務的執(zhí)行只能串行執(zhí)行,一個任務執(zhí)行時間過長的話會影響其他任務(性能非常差),再比如發(fā)生異常時任務直接停止(Timer
只捕獲了 InterruptedException
)。
Timer
類上的有一段注釋是這樣寫的:
* This class does not offer real-time guarantees: it schedules
* tasks using the <tt>Object.wait(long)</tt> method.
*Java 5.0 introduced the {@code java.util.concurrent} package and
* one of the concurrency utilities therein is the {@link
* java.util.concurrent.ScheduledThreadPoolExecutor
* ScheduledThreadPoolExecutor} which is a thread pool for repeatedly
* executing tasks at a given rate or delay. It is effectively a more
* versatile replacement for the {@code Timer}/{@code TimerTask}
* combination, as it allows multiple service threads, accepts various
* time units, and doesn't require subclassing {@code TimerTask} (just
* implement {@code Runnable}). Configuring {@code
* ScheduledThreadPoolExecutor} with one thread makes it equivalent to
* {@code Timer}.
大概的意思就是:ScheduledThreadPoolExecutor
支持多線程執(zhí)行定時任務并且功能更強大,是 Timer
的替代品。
ScheduledExecutorService
是一個接口,有多個實現(xiàn)類,比較常用的是 ScheduledThreadPoolExecutor
。
ScheduledThreadPoolExecutor
本身就是一個線程池,支持任務并發(fā)執(zhí)行。并且,其內部使用 DelayQueue
作為任務隊列。
// 示例代碼:
TimerTask repeatedTask = new TimerTask() {
@SneakyThrows
public void run() {
System.out.println('當前時間: ' + new Date() + 'n' +
'線程名稱: ' + Thread.currentThread().getName());
}
};
System.out.println('當前時間: ' + new Date() + 'n' +
'線程名稱: ' + Thread.currentThread().getName());
ScheduledExecutorService executor = Executors.newScheduledThreadPool(3);
long delay = 1000L;
long period = 1000L;
executor.scheduleAtFixedRate(repeatedTask, delay, period, TimeUnit.MILLISECONDS);
Thread.sleep(delay + period * 5);
executor.shutdown();
//輸出:
當前時間: Fri May 28 15:40:46 CST 2021n線程名稱: main
當前時間: Fri May 28 15:40:47 CST 2021n線程名稱: pool-1-thread-1
當前時間: Fri May 28 15:40:48 CST 2021n線程名稱: pool-1-thread-1
當前時間: Fri May 28 15:40:49 CST 2021n線程名稱: pool-1-thread-2
當前時間: Fri May 28 15:40:50 CST 2021n線程名稱: pool-1-thread-2
當前時間: Fri May 28 15:40:51 CST 2021n線程名稱: pool-1-thread-2
當前時間: Fri May 28 15:40:52 CST 2021n線程名稱: pool-1-thread-2
不論是使用 Timer
還是 ScheduledExecutorService
都無法使用 Cron 表達式指定任務執(zhí)行的具體時間。
我們直接通過 Spring 提供的 @Scheduled
注解即可定義定時任務,非常方便!
/**
* cron:使用Cron表達式。 每分鐘的1,2秒運行
*/
@Scheduled(cron = '1-2 * * * * ? ')
public void reportCurrentTimeWithCronExpression() {
log.info('Cron Expression: The time is now {}', dateFormat.format(new Date()));
}
我在大學那會做的一個 SSM 的企業(yè)級項目,就是用的 Spring Task 來做的定時任務。
并且,Spring Task 還是支持 Cron 表達式 的。Cron 表達式主要用于定時作業(yè)(定時任務)系統(tǒng)定義執(zhí)行時間或執(zhí)行頻率的表達式,非常厲害,你可以通過 Cron 表達式進行設置定時任務每天或者每個月什么時候執(zhí)行等等操作。咱們要學習定時任務的話,Cron 表達式是一定是要重點關注的。推薦一個在線 Cron 表達式生成器:http://cron.qqe2.com/ 。
但是,Spring 自帶的定時調度只支持單機,并且提供的功能比較單一。之前寫過一篇文章:《5 分鐘搞懂如何在 Spring Boot 中 Schedule Tasks》 ,不了解的小伙伴可以參考一下。
Spring Task 底層是基于 JDK 的 ScheduledThreadPoolExecutor
線程池來實現(xiàn)的。
優(yōu)缺點總結:
Kafka、Dubbo、ZooKeeper、Netty 、Caffeine 、Akka 中都有對時間輪的實現(xiàn)。
時間輪簡單來說就是一個環(huán)形的隊列(底層一般基于數(shù)組實現(xiàn)),隊列中的每一個元素(時間格)都可以存放一個定時任務列表。
時間輪中的每個時間格代表了時間輪的基本時間跨度或者說時間精度,加入時間一秒走一個時間格的話,那么這個時間輪的最高精度就是 1 秒(也就是說 3 s 和 3.9s 會在同一個時間格中)。
下圖是一個有 12 個時間格的時間輪,轉完一圈需要 12 s。當我們需要新建一個 3s 后執(zhí)行的定時任務,只需要將定時任務放在下標為 3 的時間格中即可。當我們需要新建一個 9s 后執(zhí)行的定時任務,只需要將定時任務放在下標為 9 的時間格中即可。
那當我們需要創(chuàng)建一個 13s 后執(zhí)行的定時任務怎么辦呢?這個時候可以引入一叫做 圈數(shù)/輪數(shù) 的概念,也就是說這個任務還是放在下標為 3 的時間格中, 不過它的圈數(shù)為 2 。
除了增加圈數(shù)這種方法之外,還有一種 多層次時間輪 (類似手表),Kafka 采用的就是這種方案。
針對下圖的時間輪,我來舉一個例子便于大家理解。
上圖的時間輪,第 1 層的時間精度為 1 ,第 2 層的時間精度為 20 ,第 3 層的時間精度為 400。假如我們需要添加一個 350s 后執(zhí)行的任務 A 的話(當前時間是 0s),這個任務會被放在第 2 層(因為第二層的時間跨度為 20*20=400>350)的第 350/20=17 個時間格子。
當?shù)谝粚愚D了 17 圈之后,時間過去了 340s ,第 2 層的指針此時來到第 17 個時間格子。此時,第 2 層第 17 個格子的任務會被移動到第 1 層。
任務 A 當前是 10s 之后執(zhí)行,因此它會被移動到第 1 層的第 10 個時間格子。
這里在層與層之間的移動也叫做時間輪的升降級。參考手表來理解就好!
時間輪比較適合任務數(shù)量比較多的定時任務場景,它的任務寫入和執(zhí)行的時間復雜度都是 0(1)。
上面提到的一些定時任務的解決方案都是在單機下執(zhí)行的,適用于比較簡單的定時任務場景比如每天凌晨備份一次數(shù)據(jù)。
如果我們需要一些高級特性比如支持任務在分布式場景下的分片和高可用的話,我們就需要用到分布式任務調度框架了。
通常情況下,一個定時任務的執(zhí)行往往涉及到下面這些角色:
一個很火的開源任務調度框架,完全由Java
寫成。Quartz
可以說是 Java 定時任務領域的老大哥或者說參考標準,其他的任務調度框架基本都是基于 Quartz
開發(fā)的,比如當當網的elastic-job
就是基于quartz
二次開發(fā)之后的分布式調度解決方案。
使用 Quartz
可以很方便地與 Spring
集成,并且支持動態(tài)添加任務和集群。但是,Quartz
使用起來也比較麻煩,API 繁瑣。
并且,Quzrtz
并沒有內置 UI 管理控制臺,不過你可以使用 quartzui 這個開源項目來解決這個問題。
另外,Quartz
雖然也支持分布式任務。但是,它是在數(shù)據(jù)庫層面,通過數(shù)據(jù)庫的鎖機制做的,有非常多的弊端比如系統(tǒng)侵入性嚴重、節(jié)點負載不均衡。有點偽分布式的味道。
優(yōu)缺點總結:
Spring
集成,并且支持動態(tài)添加任務和集群。Elastic-Job
是當當網開源的一個基于Quartz
和ZooKeeper
的分布式調度解決方案,由兩個相互獨立的子項目 Elastic-Job-Lite
和 Elastic-Job-Cloud
組成,一般我們只要使用 Elastic-Job-Lite
就好。
ElasticJob
支持任務在分布式場景下的分片和高可用、任務可視化管理等功能。
ElasticJob-Lite 的架構設計如下圖所示:
從上圖可以看出,Elastic-Job
沒有調度中心這一概念,而是使用 ZooKeeper
作為注冊中心,注冊中心負責協(xié)調分配任務到不同的節(jié)點上。
Elastic-Job 中的定時調度都是由執(zhí)行器自行觸發(fā),這種設計也被稱為去中心化設計(調度和處理都是執(zhí)行器單獨完成)。
@Component
@ElasticJobConf(name = 'dayJob', cron = '0/10 * * * * ?', shardingTotalCount = 2,
shardingItemParameters = '0=AAAA,1=BBBB', description = '簡單任務', failover = true)
public class TestJob implements SimpleJob {
@Override
public void execute(ShardingContext shardingContext) {
log.info('TestJob任務名:【{}】, 片數(shù):【{}】, param=【{}】', shardingContext.getJobName(), shardingContext.getShardingTotalCount(),
shardingContext.getShardingParameter());
}
}
相關地址:
優(yōu)缺點總結:
Spring
集成、支持分布式、支持集群、性能不錯XXL-JOB
于 2015 年開源,是一款優(yōu)秀的輕量級分布式任務調度框架,支持任務可視化管理、彈性擴容縮容、任務失敗重試和告警、任務分片等功能,
根據(jù) XXL-JOB
官網介紹,其解決了很多 Quartz
的不足。
XXL-JOB
的架構設計如下圖所示:
從上圖可以看出,XXL-JOB
由 調度中心 和 執(zhí)行器 兩大部分組成。調度中心主要負責任務管理、執(zhí)行器管理以及日志管理。執(zhí)行器主要是接收調度信號并處理。另外,調度中心進行任務調度時,是通過自研 RPC 來實現(xiàn)的。
不同于 Elastic-Job
的去中心化設計, XXL-JOB
的這種設計也被稱為中心化設計(調度中心調度多個執(zhí)行器執(zhí)行任務)。
和 Quzrtz
類似 XXL-JOB
也是基于數(shù)據(jù)庫鎖調度任務,存在性能瓶頸。不過,一般在任務量不是特別大的情況下,沒有什么影響的,可以滿足絕大部分公司的要求。
不要被 XXL-JOB
的架構圖給嚇著了,實際上,我們要用 XXL-JOB
的話,只需要重寫 IJobHandler
自定義任務執(zhí)行邏輯就可以了,非常易用!
@JobHandler(value='myApiJobHandler')
@Component
public class MyApiJobHandler extends IJobHandler {
@Override
public ReturnT<String> execute(String param) throws Exception {
//......
return ReturnT.SUCCESS;
}
}
還可以直接基于注解定義任務。
@XxlJob('myAnnotationJobHandler')
public ReturnT<String> myAnnotationJobHandler(String param) throws Exception {
//......
return ReturnT.SUCCESS;
}
相關地址:
優(yōu)缺點總結:
非常值得關注的一個分布式任務調度框架,分布式任務調度領域的新星。目前,已經有很多公司接入比如 OPPO、京東、中通、思科。
這個框架的誕生也挺有意思的,PowerJob 的作者當時在阿里巴巴實習過,阿里巴巴那會使用的是內部自研的 SchedulerX(阿里云付費產品)。實習期滿之后,PowerJob 的作者離開了阿里巴巴。想著說自研一個 SchedulerX,防止哪天 SchedulerX 滿足不了需求,于是 PowerJob 就誕生了。
更多關于 PowerJob 的故事,小伙伴們可以去看看 PowerJob 作者的視頻 《我和我的任務調度中間件》。簡單點概括就是:“游戲沒啥意思了,我要扛起了新一代分布式任務調度與計算框架的大旗!”。
由于 SchedulerX 屬于人民幣產品,我這里就不過多介紹。PowerJob 官方也對比過其和 QuartZ、XXL-JOB 以及 SchedulerX。
這篇文章中,我主要介紹了:
這篇文章并沒有介紹到實際使用,但是,并不代表實際使用不重要。我在寫這篇文章之前,已經動手寫過相應的 Demo。像 Quartz,我在大學那會就用過。不過,當時用的是 Spring 。為了能夠更好地體驗,我自己又在 Spring Boot 上實際體驗了一下。如果你并沒有實際使用某個框架,就直接說它并不好用的話,是站不住腳的。
最后,這篇文章要感謝艿艿的幫助,寫這篇文章的時候向艿艿詢問過一些問題。推薦一篇艿艿寫的偏實戰(zhàn)類型的硬核文章:《Spring Job?Quartz?XXL-Job?年輕人才做選擇,艿艿全莽~》 。