定時任務 通常會存在 中大型企業級 項目中,爲了減小 服務器、數據庫 的壓力,每每會以 定時任務 的方式去完成某些業務邏輯。java
常見的就是 金融服務系統 推送回調,通常支付系統訂單在沒有收到成功的回調返回內容時會 持續性的回調,這種回調通常都是 定時任務 來完成。web
還有就是 報表的生成,咱們通常會在客戶 訪問量小 時完成這個操做,也能夠採用 定時任務 來完成。spring
這是 Java
自帶的 java.util.Timer
類,這個類容許調度一個名爲 java.util.TimerTask
任務。使用這種方式可讓你的程序按照某一個 頻度 執行,但不能在 指定時間 運行。如今通常用的較少。數據庫
JDK
自帶的一個類,是基於 線程池 設計的定時任務類,每一個 調度任務 都會分配到 線程池 中的一個 線程 去執行。也就是說,任務是 併發執行,互不影響的。編程
Spring 3.0
之後自帶的 Task
,支持 多線程 調度,能夠將它當作一個 輕量級 的 Quartz
,並且使用起來比 Quartz
簡單許多,可是適用於 單節點 的 定時任務調度。後端
這是一個 功能比較強大 的的調度器,可讓你的程序在指定時間執行,也能夠按照某一個頻度執行,配置起來 稍顯複雜。Quartz
功能強大,能夠結合 數據庫 作 持久化,進行 分佈式 的 任務延時調度。緩存
Cron
表達式是一個字符串,字符串以 5
或 6
個 空格 隔開,分爲 6
或 7
個 域,每個域表明一個含義,Cron
有以下兩種語法格式:springboot
- Seconds Minutes Hours DayofMonth Month DayofWeek Year
- Seconds Minutes Hours DayofMonth Month DayofWeek
每一個域對應的含義、域值範圍和特殊表示符,從左到右依次以下:bash
字段 | 容許值 | 容許的特殊字符 |
---|---|---|
秒 | 0-59 | , - * / |
分 | 0-59 | , - * / |
小時 | 0-23 | , - * / |
日期 | 1-31 | , - * / L W C |
月份 | 1-12 或者 JAN-DEC | , - * / |
星期 | 1-7 或者 SUN-SAT | , - * / L C # |
年(可選) | 留空, 1970-2099 | , - * / |
如上面的表達式所示:服務器
""字符: 被用來指定全部的值。如:在分鐘的字段域裏表示"每分鐘"。
"-"字符: 被用來指定一個範圍。如:"10-12" 在小時域意味着 "10點、11點、12點"。
","字符: 被用來指定另外的值。如:"MON,WED,FRI" 在星期域裏表示 "星期1、星期3、星期五"。
"?"字符: 只在日期域和星期域中使用。它被用來指定"非明確的值"。當你須要經過在這兩個域中的一個來指定一些東西的時候,它是有用的。看下面的例子你就會明白。
"L"字符: 指定在月或者星期中的某天(最後一天)。即 "Last" 的縮寫。可是在星期和月中 "L" 表示不一樣的意思,如:在月子段中 "L" 指月份的最後一天 - 1月31日,2月28日。
"W"字符: 只能用在月份字段中,該字段指定了離指定日期最近的那個星期日。
"#"字符: 只能用在星期字段,該字段指定了第幾個星期 value 在某月中
每個元素均可以顯式地規定一個值(如 6
),一個區間(如 9-12
),一個列表(如 9,11,13
)或一個通配符(如 *
)。"月份中的日期" 和 "星期中的日期" 這兩個元素是 互斥的,所以應該經過設置一個 問號(?
)來代表你不想設置的那個字段。下表顯示了一些 cron
表達式的 例子 和它們的意義:
表達式 | 意義 |
---|---|
"0 0 12 * * ?" | 天天中午12點觸發 |
"0 15 10 ? * *" | 天天上午10:15觸發 |
"0 15 10 * * ?" | 天天上午10:15觸發 |
"0 15 10 * * ? *" | 天天上午10:15觸發 |
"0 15 10 * * ? 2005" | 2005年的天天上午10:15觸發 |
"0 * 14 * * ?" | 在天天下午2點到下午2:59期間的每1分鐘觸發 |
"0 0/5 14 * * ?" | 在天天下午2點到下午2:55期間的每5分鐘觸發 |
"0 0/5 14,18 * * ?" | 在天天下午2點到2:55期間和下午6點到6:55期間的每5分鐘觸發 |
"0 0-5 14 * * ?" | 在天天下午2點到下午2:05期間的每1分鐘觸發 |
"0 10,44 14 ? 3 WED" | 每一年三月的星期三的下午2:10和2:44觸發 |
"0 15 10 ? * MON-FRI" | 週一至週五的上午10:15觸發 |
"0 15 10 15 * ?" | 每個月15日上午10:15觸發 |
"0 15 10 L * ?" | 每個月最後一日的上午10:15觸發 |
"0 15 10 ? * 6L" | 每個月的最後一個星期五上午10:15觸發 |
"0 15 10 ? * 6L 2002-2005" | 2002年至2005年的每個月的最後一個星期五上午10:15觸發 |
"0 15 10 ? * 6#3" | 每個月的第三個星期五上午10:15觸發 |
0 6 * * * | 天天早上6點 |
0 /2 * * | 每兩個小時 |
0 23-7/2,8 * * * | 晚上11點到早上8點之間每兩個小時,早上八點 |
0 11 4 * 1-3 | 每月的4號和每一個禮拜的禮拜一到禮拜三的早上11點 |
0 4 1 1 * | 1月1日早上4點 |
利用 Spring Initializer
建立一個 gradle
項目 spring-boot-scheduler-task-management
,建立時添加相關依賴。獲得的初始 build.gradle
以下:
buildscript {
ext {
springBootVersion = '2.0.3.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
group = 'io.ostenant.springboot.sample'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
compile('org.springframework.boot:spring-boot-starter')
compile('org.springframework.boot:spring-boot-starter-web')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
複製代碼
在 Spring Boot
入口類上配置 @EnableScheduling
註解開啓 Spring
自帶的定時處理功能。
@SpringBootApplication
@EnableScheduling
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
複製代碼
這個 API
目前在項目中不多用,直接給出示例代碼。具體的介紹能夠查看 API
。Timer
的內部只有 一個線程,若是有 多個任務 的話就會 順序執行,這樣任務的 延遲時間 和 循環時間 就會出現問題。
TimerService.java
public class TimerService {
private static final Logger LOGGER = LoggerFactory.getLogger(TimerService.class);
private AtomicLong counter = new AtomicLong();
public void schedule() {
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
long count = counter.incrementAndGet();
LOGGER.info("Schedule timerTask {} times", count);
}
};
Timer timer = new Timer();
timer.schedule(timerTask, 1000L, 10 * 1000L;
}
}
複製代碼
上面的代碼定義了一個 TimerTask
,在 TimerTask
中累加 執行次數,並經過 slf4j
進行打印 (自帶執行時間)。而後經過 Timer
調度工具類調度 TimerTask
任務,設置 初始化延遲時間 爲 1s
,定時執行間隔 爲 10s
,測試代碼以下:
public static void main(String[] args) {
TimerService timerService = new TimerService();
timerService.schedule();
}
複製代碼
觀察測試結果,可以發現 TimerTask
配置的任務每隔 10s
被執行了一次,執行線程默認都是 Timer-0
這個線程。
17:48:18.731 [Timer-0] INFO io.ostenant.springboot.sample.timer.TimerService - Schedule timerTask 1 times
17:48:28.730 [Timer-0] INFO io.ostenant.springboot.sample.timer.TimerService - Schedule timerTask 2 times
17:48:38.736 [Timer-0] INFO io.ostenant.springboot.sample.timer.TimerService - Schedule timerTask 3 times
17:48:48.738 [Timer-0] INFO io.ostenant.springboot.sample.timer.TimerService - Schedule timerTask 4 times
17:48:58.743 [Timer-0] INFO io.ostenant.springboot.sample.timer.TimerService - Schedule timerTask 5 times
複製代碼
ScheduledExecutorService
是 延時執行 的線程池,對於 多線程 環境下的 定時任務,推薦用 ScheduledExecutorService
代替 Timer
定時器。
建立一個線程數量爲 4
的 任務線程池,同一時刻並向它提交 4
個定時任務,用於測試延時任務的 併發處理。執行 ScheduledExecutorService
的 scheduleWithFixedDelay()
方法,設置任務線程池的 初始任務延遲時間 爲 2
秒,並在上一次 執行完畢時間點 以後 10
秒再執行下一次任務。
public void scheduleWithFixedDelay() {
ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(4);
for (int i = 0; i < 4; i++) {
scheduledExecutor.scheduleWithFixedDelay(() -> {
try {
TimeUnit.MILLISECONDS.sleep(10 * 1000L);
} catch (InterruptedException e) {
LOGGER.error("Interrupted exception", e);
}
long count = counter.incrementAndGet();
LOGGER.info("Schedule executor {} times with fixed delay", count);
}, 2000L, 10 * 1000L, TimeUnit.MILLISECONDS);
}
LOGGER.info("Start to schedule");
}
複製代碼
測試結果以下,咱們能夠發現每隔 20
秒的時間間隔,就會有 4
個定時任務同時執行。由於在任務線程池初始化時,咱們同時向線程池提交了 4
個任務,這 四個任務 會徹底利用線程池中的 4
個線程進行任務執行。
20
秒是怎麼來的?首先每一個任務的 時間間隔 設置爲 10
秒。其次由於採用的是 withFixedDelay
策略,即當前任務執行的 結束時間,做爲下次延時任務的 開始計時節點,而且每一個任務在執行過程當中睡眠了 10
秒的時間,累計起來就是 20
秒的時間。
19:42:02.444 [main] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Start to schedule
19:42:14.449 [pool-1-thread-1] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 3 times with fixed delay
19:42:14.449 [pool-1-thread-2] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 1 times with fixed delay
19:42:14.449 [pool-1-thread-3] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 2 times with fixed delay
19:42:14.449 [pool-1-thread-4] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 4 times with fixed delay
19:42:34.458 [pool-1-thread-4] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 7 times with fixed delay
19:42:34.458 [pool-1-thread-3] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 5 times with fixed delay
19:42:34.458 [pool-1-thread-2] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 8 times with fixed delay
19:42:34.458 [pool-1-thread-1] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 6 times with fixed delay
複製代碼
建立一個線程數量爲 4
的 任務線程池,同一時刻並向它提交 4
個定時任務,用於測試延時任務的 併發處理。每一個任務分別執行 ScheduledExecutorService
的 scheduleAtFixedRate()
方法,設置任務線程池的 初始任務延遲時間 爲 2
秒,並在上一次 開始執行時間點 以後 10
秒再執行下一次任務。
public void scheduleAtFixedRate() {
ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(4);
for (int i = 0; i < 4; i++) {
scheduledExecutor.scheduleAtFixedRate(() -> {
long count = counter.incrementAndGet();
LOGGER.info("Schedule executor {} times at fixed rate", count);
}, 2000L, 10 * 1000L, TimeUnit.MILLISECONDS);
}
LOGGER.info("Start to schedule");
}
複製代碼
測試結果以下,咱們能夠發現每隔 10
秒的時間間隔,就會有 4
個定時任務同時執行,由於在任務線程池初始化時,咱們同時向線程池提交了 4
個任務,這 四個任務 會徹底利用線程池中的 4
個線程進行任務執行。
19:31:46.837 [main] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Start to schedule
19:31:48.840 [pool-1-thread-1] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 1 times at fixed rate
19:31:48.840 [pool-1-thread-3] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 3 times at fixed rate
19:31:48.840 [pool-1-thread-2] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 2 times at fixed rate
19:31:48.840 [pool-1-thread-4] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 4 times at fixed rate
19:31:58.839 [pool-1-thread-2] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 6 times at fixed rate
19:31:58.840 [pool-1-thread-4] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 8 times at fixed rate
19:31:58.839 [pool-1-thread-3] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 7 times at fixed rate
19:31:58.839 [pool-1-thread-1] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 5 times at fixed rate
複製代碼
Spring
提供了 @Scheduled
註解來實現 定時任務,@Scheduled
參數能夠接受 兩種 定時的設置,一種是咱們經常使用的 格林時間表達式 cron = "*/10 * * * * *"
,另外一種是 fixedRate = 10 * 1000L
,兩種都表示每隔 10
秒執行一次目標任務。
參數說明:
10
秒再執行。@Scheduled(fixedRate = 10 * 1000L)
public void scheduleAtFixedRate() throws Exception {
long count = counter.incrementAndGet();
LOGGER.info("Schedule executor {} times at fixed rate", count);
}
複製代碼
10
秒再執行。@Scheduled(fixedDelay = 10 * 1000L)
public void scheduleWithFixedDelay() throws Exception {
try {
TimeUnit.MILLISECONDS.sleep(10 * 1000L);
} catch (InterruptedException e) {
LOGGER.error("Interrupted exception", e);
}
long count = counter.incrementAndGet();
LOGGER.info("Schedule executor {} times with fixed delay", count);
}
複製代碼
2
秒後執行,以後按 fixedRate
的規則每 10
秒執行一次。@Scheduled(initialDelay = 2000L, fixedDelay = 10 * 1000L)
public void scheduleWithinitialDelayAndFixedDelay() throws Exception {
try {
TimeUnit.MILLISECONDS.sleep(10 * 1000L);
} catch (InterruptedException e) {
LOGGER.error("Interrupted exception", e);
}
long count = counter.incrementAndGet();
LOGGER.info("Schedule executor {} times with fixed delay", count);
}
複製代碼
cron
表達式定義,每隔 10
秒執行一次。@Scheduled(cron = "0/10 * * * * *")
public void scheduleWithCronExpression() throws Exception {
long count = counter.incrementAndGet();
LOGGER.info("Schedule executor {} times with ", count);
}
複製代碼
完整的代碼以下:
SpringTaskService.java
@Component
public class SpringTaskService {
private static final Logger LOGGER = LoggerFactory.getLogger(SpringTaskService.class);
private AtomicLong counter = new AtomicLong();
@Scheduled(fixedDelay = 10 * 1000L)
public void scheduleWithFixedDelay() throws Exception {
try {
TimeUnit.MILLISECONDS.sleep(10 * 1000L);
} catch (InterruptedException e) {
LOGGER.error("Interrupted exception", e);
}
long count = counter.incrementAndGet();
LOGGER.info("Schedule executor {} times with fixed delay", count);
}
@Scheduled(initialDelay = 2000L, fixedDelay = 10 * 1000L)
public void scheduleWithinitialDelayAndFixedDelay() throws Exception {
try {
TimeUnit.MILLISECONDS.sleep(10 * 1000L);
} catch (InterruptedException e) {
LOGGER.error("Interrupted exception", e);
}
long count = counter.incrementAndGet();
LOGGER.info("Schedule executor {} times with fixed delay", count);
}
@Scheduled(fixedRate = 10 * 1000L)
public void scheduleAtFixedRate() throws Exception {
long count = counter.incrementAndGet();
LOGGER.info("Schedule executor {} times at fixed rate", count);
}
@Scheduled(cron = "0/10 * * * * *")
public void scheduleWithCronExpression() throws Exception {
long count = counter.incrementAndGet();
LOGGER.info("Schedule executor {} times with ", count);
}
}
複製代碼
查看日誌,任務每 20
秒的時間間隔執行一次。每次定時任務在上次 執行完畢時間點 以後 10
秒再執行,在任務中設置 睡眠時間 爲 10
秒。這裏只驗證了 @Scheduled(initialDelay = 2000L, fixedDelay = 10 * 1000L)。
2018-06-25 18:00:53.051 INFO 5190 --- [pool-1-thread-1] i.o.s.sample.spring.SpringTaskService : Schedule executor 1 times with fixed delay
2018-06-25 18:01:13.056 INFO 5190 --- [pool-1-thread-1] i.o.s.sample.spring.SpringTaskService : Schedule executor 2 times with fixed delay
2018-06-25 18:01:33.061 INFO 5190 --- [pool-1-thread-1] i.o.s.sample.spring.SpringTaskService : Schedule executor 3 times with fixed delay
2018-06-25 18:01:53.071 INFO 5190 --- [pool-1-thread-1] i.o.s.sample.spring.SpringTaskService : Schedule executor 4 times with fixed delay
2018-06-25 18:02:13.079 INFO 5190 --- [pool-1-thread-1] i.o.s.sample.spring.SpringTaskService : Schedule executor 5 times with fixed delay
複製代碼
上述配置都是基於 單線程 的任務調度,如何引入 多線程 提升 延時任務 的 併發處理 能力?
Spring Boot
提供了一個 SchedulingConfigurer
配置接口。咱們經過 ScheduleConfig
配置文件實現 ScheduleConfiguration
接口,並重寫 configureTasks()
方法,向 ScheduledTaskRegistrar
註冊一個 ThreadPoolTaskScheduler
任務線程對象便可。
@Configuration
public class ScheduleConfiguration implements SchedulingConfigurer {
private static final Logger LOGGER = LoggerFactory.getLogger(ScheduleConfiguration.class);
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setTaskScheduler(taskScheduler());
}
@Bean
public ThreadPoolTaskScheduler taskScheduler() {
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setPoolSize(4);
taskScheduler.setWaitForTasksToCompleteOnShutdown(true);
taskScheduler.setThreadNamePrefix("schedule");
taskScheduler.setRemoveOnCancelPolicy(true);
taskScheduler.setErrorHandler(t -> LOGGER.error("Error occurs", t));
return taskScheduler;
}
}
複製代碼
啓動 Spring Boot
引用,上面 SpringTaskService
配置的 4
個定時任務會同時生效。
2018-06-20 20:37:50.746 INFO 8142 --- [ schedule1] i.o.s.sample.spring.SpringTaskService : Schedule executor 1 times at fixed rate
2018-06-20 20:38:00.001 INFO 8142 --- [ schedule3] i.o.s.sample.spring.SpringTaskService : Schedule executor 2 times with
2018-06-20 20:38:00.751 INFO 8142 --- [ schedule1] i.o.s.sample.spring.SpringTaskService : Schedule executor 3 times at fixed rate
2018-06-20 20:38:02.748 INFO 8142 --- [ schedule2] i.o.s.sample.spring.SpringTaskService : Schedule executor 4 times with fixed delay
2018-06-20 20:38:10.005 INFO 8142 --- [ schedule4] i.o.s.sample.spring.SpringTaskService : Schedule executor 5 times with
2018-06-20 20:38:10.747 INFO 8142 --- [ schedule3] i.o.s.sample.spring.SpringTaskService : Schedule executor 6 times at fixed rate
2018-06-20 20:38:20.002 INFO 8142 --- [ schedule2] i.o.s.sample.spring.SpringTaskService : Schedule executor 7 times with
2018-06-20 20:38:20.747 INFO 8142 --- [ schedule4] i.o.s.sample.spring.SpringTaskService : Schedule executor 8 times at fixed rate
複製代碼
觀察日誌,線程名前綴 爲 schedule
,能夠發現 Spring Task
將 @Scheduled
註解配置的 4
個任務,分發給咱們配置的 ThreadPoolTaskScheduler
中的 4
個線程併發執行。
本文介紹了基於單節點的定時任務調度及實現,包括 JDK
原生的 Timer
和 ScheduledExecutorService
,以及 Spring 3.0
之後自帶的基於註解的 Spring Task
任務調度方式。除此以外,重點闡述了基於 固定延時、固定頻率 和 cron
表達式 的不一樣之處,並對 ScheduledExecutorService
和 Spring Scheduler
的 線程池併發處理 進行了測試。
歡迎關注技術公衆號: 零壹技術棧
本賬號將持續分享後端技術乾貨,包括虛擬機基礎,多線程編程,高性能框架,異步、緩存和消息中間件,分佈式和微服務,架構學習和進階等學習資料和文章。