在工程中時常會遇到一些需求汉柒,例如定時刷新一下配置臀防、隔一段時間檢查下網(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)有了比較好的解決方案,大致有以下幾種:
- Spring Scheduler 框架
- 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>TaskExecutor
與 TaskScheduler
的區(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 配置
- 定義 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 的初始化。
- 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。
-
task:scheduler
定義一個ThreadPoolTaskScheduler
, 提供唯一參數(shù)指定了池大小益兄。-
pool-size
: 任務(wù)池大小锻梳。池的大小控制著task:scheduled
的執(zhí)行,例如以上例子净捅,若 pool-size = 1疑枯,則兩個 task 只能依次運(yùn)行,無法并行執(zhí)行蛔六。
-
-
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á)式請參考附錄
注解配置
- 定義 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());
}
}
- 添加注解驅(qū)動配置
<task:annotation-driven />
更多細(xì)節(jié)桃漾,建議查看 xsd 文件和 Spring 源碼
Scheduling 框架源碼分析
Scheduleing 部分的源碼是比較清晰和簡單的。
Scheduleing 中最重要的類是 ThreadPoolTaskScheduler
拟逮,這是使用 schedule 時的默認(rèn)的線程池撬统,其類圖如下:
在 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)方法,此處不再贅述脾猛。
存在的問題
-
注意線程池和任務(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ù)。
注意
fixedDelay
和fixedRate
之間的區(qū)別专肪。前者是任務(wù)完成后計(jì)時糙箍,后者是從任務(wù)開始時計(jì)時。如果每分鐘執(zhí)行一次任務(wù)牵祟,但任務(wù)執(zhí)行時間大于一分鐘的話,fixedRate
將會出現(xiàn)任務(wù)在隊(duì)列中的堆積的問題多實(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 put10
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”. And5/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 having0
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 valueL
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 means7
orSAT
. But if used in the day-of-week field after another value, it means “the last xxx day of the month” - for example6L
means “the last friday of the month”. You can also specify an offset from the last day of the month, such asL-3
which would mean the third-to-last day of the calendar month. When using theL
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 specify15W
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 specify1W
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. TheW
character can only be specified when the day-of-month is a single day, not a range or list of days.
The
L
andW
characters can also be combined in the day-of-month field to yieldLW
, which translates to "last weekday of the month".
-
#
- used to specify “the nth” XXX day of the month. For example, the value of6#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 and4#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 asmon
.
舉例
以下是摘自 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