Spring Scheduler 與 Quartz 進(jìn)階

在工程中時常會遇到一些需求汉柒,例如定時刷新一下配置臀防、隔一段時間檢查下網(wǎng)絡(luò)狀態(tài)并發(fā)送郵件等諸如此類的定時任務(wù)。

定時任務(wù)本質(zhì)就是一個異步的線程呕屎,線程可以查詢或修改并執(zhí)行一系列的操作磺陡。由于本質(zhì)是線程,在 Java 中可以自行編寫一個線程池對定時任務(wù)進(jìn)行控制漠畜,但這樣效率太低了币他,且功能有限,屬于重復(fù)造輪子憔狞。

事實(shí)上蝴悉,當(dāng)前實(shí)現(xiàn)定時任務(wù)已經(jīng)有了比較好的解決方案,大致有以下幾種:

  1. Spring Scheduler 框架
  2. Quartz 框架瘾敢,功能強(qiáng)大拍冠,配置靈活(自然更繁瑣 =。=)

本文將總結(jié) Spring 定時任務(wù)簇抵。Let's Begin

Spring Scheduler

Spring 于 3.0 版本引入了 TaskScheduler庆杜,相比較于 Spring 2.0 時的 TaskExecutor,簡化了對線程的管理碟摆,線程均由框架管理晃财,不需要指定調(diào)度器 scheduler 實(shí)現(xiàn),可以自定義調(diào)度的間隔和時間典蜕,相對的對線程池等的功能較簡單断盛。


<u>TaskExecutorTaskScheduler 的區(qū)別</u>

<u>Scheduler 只是任務(wù)的簡單調(diào)度,可以指定任務(wù)的執(zhí)行時間愉舔,但對任務(wù)隊(duì)列和線程池的管控較弱钢猛,定時調(diào)度的主要目的還是控制執(zhí)行時間。</u>

<u> Executor 則提供了更細(xì)化線程池配置轩缤,如等待隊(duì)列容量命迈、存活時間控制,也支持異步執(zhí)行任務(wù)典奉。TaskExecutor 可以理解為是 Spring 框架下的線程池管理躺翻,是從 JDK 5 中對 Executor 接口的抽離,主要職責(zé)并不在于幾點(diǎn)幾分幾秒去執(zhí)行什么任務(wù)卫玖。</u>


定時調(diào)度實(shí)現(xiàn)依賴于 spring-context 包公你。因此 Maven 中需加入以下依賴配置。

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>${spring.version}</version>
</dependency>

與定時任務(wù)相關(guān)的代碼假瞬,位于包 org.springframework.scheduling 下陕靠。源碼并不復(fù)雜迂尝,可以花點(diǎn)時間讀一讀。

<br />

Spring TaskScheduler實(shí)踐

作為 Spring 下的框架剪芥,一些 Spring 的基本配置就此略過垄开。

XML 配置

  1. 定義 Task 類
@Lazy
@Service
public class DemoTask {
    public void job1() {
        System.out.println("Task job1: " + System.currentTimeMillis());
    }

    public void job2() {
        System.out.println("Task job2: " + System.currentTimeMillis());
    }
}

其中 @Lazy 注解表明不執(zhí)行 Bean 的初始化。

  1. XML 配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:task="http://www.springframework.org/schema/task"
       xsi:schemaLocation="http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <task:scheduler id="demoTaskBean" pool-size="2"/>
    <task:scheduled-tasks scheduler="demoTaskBean">
        <task:scheduled ref="demoTask" method="job1" fixed-rate="100" initial-delay="200" cron="" fixed-delay="300"/>
        <task:scheduled ref="demoTask" method="job2" cron="*/5 * * * * ?"/>
    </task:scheduled-tasks>
</beans>

XML 的配置税肪,定義一個 scheduler溉躲,然后定義具體任務(wù) scheduled-task。

  1. task:scheduler 定義一個 ThreadPoolTaskScheduler, 提供唯一參數(shù)指定了池大小益兄。
    • pool-size: 任務(wù)池大小锻梳。池的大小控制著 task:scheduled 的執(zhí)行,例如以上例子净捅,若 pool-size = 1疑枯,則兩個 task 只能依次運(yùn)行,無法并行執(zhí)行蛔六。
  1. task:scheduled-tasks :指定所采用的任務(wù)池

    • scheduler: 定義的任務(wù)池 Bean 的 ID荆永,若不指定,則會被包裝為一個單線程 Executor
    • task:scheduled : 指定具體任務(wù)国章,至少有一個具钥,其參數(shù)如下表
參數(shù)名 說明
initial-delay 任務(wù)初始延遲,單位 milliseconds
fixed-delay 任務(wù)固定間隔時間液兽,時間自前一次完成后計(jì)算氓拼,單位 milliseconds
fixed-rate 任務(wù)固定間隔時間,時間自前一次開始后計(jì)算抵碟,單位 milliseconds
cron cron 表達(dá)式
trigger 實(shí)現(xiàn) Trigger 接口的 Bean 的引用
ref 具體 Task 的方法所在類
method 具體 Task 的方法名

cron 表達(dá)式請參考附錄

注解配置

  1. 定義 Task 類
@Service
public class DemoTask {
    @Scheduled(cron = "5 * * * * ?")
    public void job1() {
        System.out.println("Task job1: " + System.currentTimeMillis());
    }

    @Scheduled(initialDelay = 100, fixedDelay = 1000)
    public void job2() {
        System.out.println("Task job2: " + System.currentTimeMillis());
    }
}
  1. 添加注解驅(qū)動配置
<task:annotation-driven />

更多細(xì)節(jié)桃漾,建議查看 xsd 文件和 Spring 源碼


Scheduling 框架源碼分析

Scheduleing 部分的源碼是比較清晰和簡單的。

Scheduleing 中最重要的類是 ThreadPoolTaskScheduler 拟逮,這是使用 schedule 時的默認(rèn)的線程池撬统,其類圖如下:

image

ThreadPoolTaskScheduler 中定義了一些內(nèi)部變量

// ScheduledThreadPoolExecutor.setRemoveOnCancelPolicy(boolean) only available on JDK 7+
private static final boolean setRemoveOnCancelPolicyAvailable =
    ClassUtils.hasMethod(ScheduledThreadPoolExecutor.class, "setRemoveOnCancelPolicy", boolean.class);

private volatile int poolSize = 1;
private volatile boolean removeOnCancelPolicy = false;
private volatile ScheduledExecutorService scheduledExecutor;
private volatile ErrorHandler errorHandler;

需要關(guān)注的是 ScheduledExecutorService scheduledExecutor,這是一個繼承了 ExecutorService 接口的一個接口敦迄,是在 ExecutorService 基礎(chǔ)上新增了定時調(diào)度的幾個方法恋追,scheduledExecutor 的具體實(shí)現(xiàn)是 java.util.concurrent.ScheduledThreadPoolExecutor,具體可以查看 JDK 源碼罚屋。

除了定義 Executor 類苦囱,還實(shí)現(xiàn)了AsyncListenableTaskExecutor,SchedulingTaskExecutor, TaskScheduler 三個接口的相關(guān)方法,此處不再贅述脾猛。


存在的問題

  1. 注意線程池和任務(wù)數(shù)的關(guān)系

    在實(shí)踐中撕彤,若出現(xiàn)線程池?cái)?shù)小于任務(wù)數(shù)時,任務(wù)將進(jìn)入隊(duì)列,按照配置中的順序執(zhí)行羹铅。此時蚀狰,若對任務(wù)的執(zhí)行時間有嚴(yán)格要求的話,將無法被滿足职员。例如定義了兩個任務(wù)均需要在整分鐘時執(zhí)行麻蹋,任務(wù)執(zhí)行分別需要花費(fèi) 20s 和 30s。那么在一個線程池下焊切,將先執(zhí)行第一個 20s 的任務(wù)扮授,完成后再執(zhí)行第二個 30s 的任務(wù)。

  2. 注意 fixedDelayfixedRate 之間的區(qū)別专肪。前者是任務(wù)完成后計(jì)時糙箍,后者是從任務(wù)開始時計(jì)時。如果每分鐘執(zhí)行一次任務(wù)牵祟,但任務(wù)執(zhí)行時間大于一分鐘的話,fixedRate 將會出現(xiàn)任務(wù)在隊(duì)列中的堆積的問題

  3. 多實(shí)例執(zhí)行的問題抖格。由于 Spring Task 是集成與 Spring 框架之內(nèi)的诺苹,若部署采取了多實(shí)例部署方案,并設(shè)計(jì)了定時調(diào)度單一資源的任務(wù)雹拄,如定時刷新 Redis 數(shù)據(jù)收奔,定時更新 MySQL 數(shù)據(jù)等等,將會出現(xiàn)問題滓玖。解決此問題坪哄,一是放棄實(shí)例中使用 Spring Task,將定時調(diào)度單獨(dú)部署與一臺機(jī)器势篡,但這種方案的缺點(diǎn)是增加了維護(hù)的成本而且造成了單點(diǎn)故障的問題翩肌;二是采用鎖機(jī)制,一般而言可以將鎖至于緩存數(shù)據(jù)庫中禁悠,每次定時任務(wù)執(zhí)行時念祭,先請求鎖資源,若失敗碍侦,則不執(zhí)行粱坤,只有請求到鎖資源的定時任務(wù)才執(zhí)行。


進(jìn)階學(xué)學(xué) Quartz

// TBD

總結(jié)

Scheduling 是專注于定時任務(wù)調(diào)度的框架瓷产,但 Spring Task 不僅僅是這一塊站玄。事實(shí)上,Task 部分更多闡述了 TaskExecutor 的內(nèi)容濒旦,這也是 Scheduling 所依賴的株旷。

并發(fā)、線程池尔邓、異步等等灾常,均在 TaskExecutor 中做了詳細(xì)的說明霎冯,這一塊需要在之后的學(xué)習(xí)中做進(jìn)一步的總結(jié)。


附錄

Cron 表達(dá)式 (摘自 Quartz)

Field Name Mandatory Allowed Values Allowed Special Characters
Seconds YES 0-59 , - * /
Minutes YES 0-59 , - * /
Hours YES 0-23 , - * /
Day of month YES 1-31 , - * ? / L W
Month YES 1-12 or JAN-DEC , - * /
Day of week YES 1-7 or SUN-SAT , - * ? / L #
Year NO empty, 1970-2099 , - * /

因此 cron 表達(dá)式可以是 6-7 位钞瀑。

特殊字符

  • * (“all values”) - used to select all values within a field. For example, * in the minute field means “every minute”.
  • ? (“no specific value”) - useful when you need to specify something in one of the two fields in which the character is allowed, but not the other. For example, if I want my trigger to fire on a particular day of the month (say, the 10th), but don’t care what day of the week that happens to be, I would put 10 in the day-of-month field, and ? in the day-of-week field. See the examples below for clarification.
  • - - used to specify ranges. For example, 10-12 in the hour field means “the hours 10, 11 and 12”.
  • , - used to specify additional values. For example, MON,WED,FRI in the day-of-week field means “the days Monday, Wednesday, and Friday”.
  • / - used to specify increments. For example, 0/15 in the seconds field means “the seconds 0, 15, 30, and 45”. And 5/15 in the seconds field means “the seconds 5, 20, 35, and 50”. You can also specify / after the * character - in this case * is equivalent to having 0 before the /. 1/3 in the day-of-month field means “fire every 3 days starting on the first day of the month”.
  • L (“l(fā)ast”) - has different meaning in each of the two fields in which it is allowed. For example, the value L in the day-of-month field means “the last day of the month” - day 31 for January, day 28 for February on non-leap years. If used in the day-of-week field by itself, it simply means 7 or SAT. But if used in the day-of-week field after another value, it means “the last xxx day of the month” - for example 6L means “the last friday of the month”. You can also specify an offset from the last day of the month, such as L-3 which would mean the third-to-last day of the calendar month. When using the L option, it is important not to specify lists, or ranges of values, as you’ll get confusing/unexpected results.
  • W (“weekday”) - used to specify the weekday (Monday-Friday) nearest the given day. As an example, if you were to specify 15W as the value for the day-of-month field, the meaning is: “the nearest weekday to the 15th of the month”. So if the 15th is a Saturday, the trigger will fire on Friday the 14th. If the 15th is a Sunday, the trigger will fire on Monday the 16th. If the 15th is a Tuesday, then it will fire on Tuesday the 15th. However if you specify 1W as the value for day-of-month, and the 1st is a Saturday, the trigger will fire on Monday the 3rd, as it will not ‘jump’ over the boundary of a month’s days. The W character can only be specified when the day-of-month is a single day, not a range or list of days.

The L and W characters can also be combined in the day-of-month field to yield LW, which translates to "last weekday of the month".

  • # - used to specify “the nth” XXX day of the month. For example, the value of 6#3 in the day-of-week field means “the third Friday of the month” (day 6 = Friday and #3 = the 3rd one in the month). Other examples: 2#1 = the first Monday of the month and 4#5 = the fifth Wednesday of the month. Note that if you specify #5 and there is not 5 of the given day-of-week in the month, then no firing will occur that month.

The legal characters and the names of months and days of the week are not case sensitive. MON is the same as mon.

舉例

以下是摘自 Quartz 的完整例子

Expression Meaning
0 0 12 * * ? Fire at 12pm (noon) every day
0 15 10 ? * * Fire at 10:15am every day
0 15 10 * * ? Fire at 10:15am every day
0 15 10 * * ? * Fire at 10:15am every day
0 15 10 * * ? 2005 Fire at 10:15am every day during the year 2005
0 * 14 * * ? Fire every minute starting at 2pm and ending at 2:59pm, every day
0 0/5 14 * * ? Fire every 5 minutes starting at 2pm and ending at 2:55pm, every day
0 0/5 14,18 * * ? Fire every 5 minutes starting at 2pm and ending at 2:55pm, AND fire every 5 minutes starting at 6pm and ending at 6:55pm, every day
0 0-5 14 * * ? Fire every minute starting at 2pm and ending at 2:05pm, every day
0 10,44 14 ? 3 WED Fire at 2:10pm and at 2:44pm every Wednesday in the month of March.
0 15 10 ? * MON-FRI Fire at 10:15am every Monday, Tuesday, Wednesday, Thursday and Friday
0 15 10 15 * ? Fire at 10:15am on the 15th day of every month
0 15 10 L * ? Fire at 10:15am on the last day of every month
0 15 10 L-2 * ? Fire at 10:15am on the 2nd-to-last last day of every month
0 15 10 ? * 6L Fire at 10:15am on the last Friday of every month
0 15 10 ? * 6L Fire at 10:15am on the last Friday of every month
0 15 10 ? * 6L 2002-2005 Fire at 10:15am on every last friday of every month during the years 2002, 2003, 2004 and 2005
0 15 10 ? * 6#3 Fire at 10:15am on the third Friday of every month
0 0 12 1/5 * ? Fire at 12pm (noon) every 5 days every month, starting on the first day of the month.
0 11 11 11 11 ? Fire every November 11th at 11:11am.

Pay attention to the effects of '?' and '*' in the day-of-week and day-of-month fields!


參考資料

[1] Spring docs, Task Execution and Scheduling. https://docs.spring.io/autorepo/docs/spring-framework/4.2.x/spring-framework-reference/html/scheduling.html

[2] Spring ThreadPoolTaskScheduler vs ThreadPoolTaskExecutor
https://stackoverflow.com/questions/33453722/spring-threadpooltaskscheduler-vs-threadpooltaskexecutor
[3] Cron Trigger Tutorial沈撞, http://www.quartz-scheduler.org/documentation/quartz-2.2.x/tutorials/crontrigger

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市雕什,隨后出現(xiàn)的幾起案子缠俺,更是在濱河造成了極大的恐慌,老刑警劉巖贷岸,帶你破解...
    沈念sama閱讀 223,002評論 6 519
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件壹士,死亡現(xiàn)場離奇詭異,居然都是意外死亡偿警,警方通過查閱死者的電腦和手機(jī)躏救,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,357評論 3 400
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來螟蒸,“玉大人盒使,你說我怎么就攤上這事∑呦樱” “怎么了少办?”我有些...
    開封第一講書人閱讀 169,787評論 0 365
  • 文/不壞的土叔 我叫張陵,是天一觀的道長诵原。 經(jīng)常有香客問我英妓,道長,這世上最難降的妖魔是什么绍赛? 我笑而不...
    開封第一講書人閱讀 60,237評論 1 300
  • 正文 為了忘掉前任蔓纠,我火速辦了婚禮,結(jié)果婚禮上吗蚌,老公的妹妹穿的比我還像新娘贺纲。我一直安慰自己,他們只是感情好褪测,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,237評論 6 398
  • 文/花漫 我一把揭開白布猴誊。 她就那樣靜靜地躺著,像睡著了一般侮措。 火紅的嫁衣襯著肌膚如雪懈叹。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,821評論 1 314
  • 那天分扎,我揣著相機(jī)與錄音澄成,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛墨状,可吹牛的內(nèi)容都是我干的卫漫。 我是一名探鬼主播,決...
    沈念sama閱讀 41,236評論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼肾砂,長吁一口氣:“原來是場噩夢啊……” “哼列赎!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起镐确,我...
    開封第一講書人閱讀 40,196評論 0 277
  • 序言:老撾萬榮一對情侶失蹤包吝,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后源葫,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體诗越,經(jīng)...
    沈念sama閱讀 46,716評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,794評論 3 343
  • 正文 我和宋清朗相戀三年息堂,在試婚紗的時候發(fā)現(xiàn)自己被綠了嚷狞。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,928評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡荣堰,死狀恐怖床未,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情持隧,我是刑警寧澤,帶...
    沈念sama閱讀 36,583評論 5 351
  • 正文 年R本政府宣布逃片,位于F島的核電站屡拨,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏褥实。R本人自食惡果不足惜呀狼,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,264評論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望损离。 院中可真熱鬧哥艇,春花似錦、人聲如沸僻澎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,755評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽窟勃。三九已至祖乳,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間秉氧,已是汗流浹背眷昆。 一陣腳步聲響...
    開封第一講書人閱讀 33,869評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人亚斋。 一個月前我還...
    沈念sama閱讀 49,378評論 3 379
  • 正文 我出身青樓作媚,卻偏偏與公主長得像,于是被迫代替她去往敵國和親帅刊。 傳聞我的和親對象是個殘疾皇子纸泡,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,937評論 2 361

推薦閱讀更多精彩內(nèi)容

  • rljs by sennchi Timeline of History Part One The Cognitiv...
    sennchi閱讀 7,352評論 0 10
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)厚掷,斷路器弟灼,智...
    卡卡羅2017閱讀 134,720評論 18 139
  • **2014真題Directions:Read the following text. Choose the be...
    又是夜半驚坐起閱讀 9,595評論 0 23
  • 新的一天 新的開始
    aofeilin閱讀 227評論 0 0
  • 累,什么也不想說冒黑。 困田绑,想早點(diǎn)睡覺。每次回村度感覺特別倒騰抡爹。老二也是老想在外面玩掩驱,不愿意睡覺。一回來就困得不行了冬竟。...
    沄瑩閱讀 195評論 0 0