Spring Task定時任務
Spring Boot 中使用 Spring Task 實現定時任務
1. 前言
在日常專案開發中我們經常要使用定時任務。比如在凌晨進行統計結算,開啟策劃活動等等。今天我們就來看看如何在 Spring Boot 中使用 Spring 內建的定時任務。
2. 開啟定時任務
Spring Boot 預設在無任何第三方依賴的情況下使用 spring-context
模組下提供的定時任務工具 Spring Task。我們只需要使用 @EnableScheduling
註解就可以開啟相關的定時任務功能。如:
package cn.felord.schedule;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* @author felord.cn
*/
@SpringBootApplication
@EnableScheduling
public class SpringbootScheduleApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootScheduleApplication.class, args);
}
}
然後我們就可以通過註解的方式實現自定義定時任務,下面我將詳細介紹如何使用註解實現定時任務。
3. @Scheduled 註解實現定時任務
只需要定義一個 Spring Bean ,然後定義具體的定時任務邏輯方法並使用 @Scheduled
註解標記該方法即可。
package cn.felord.schedule.task;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* @author felord.cn
* @since 11:02
**/
@Component
public class TaskService {
@Scheduled(fixedDelay = 1000)
public void task() {
System.out.println("Thread Name : "
+ Thread.currentThread().getName() + " i am a task : date -> "
+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}
}
請注意:
@Scheduled
註解中一定要宣告定時任務的執行策略cron
、fixedDelay
、fixedRate
三選一。
我們來認識一下 @Scheduled
提供了四個屬性。
3.1 cron 表示式
cron
。這個我們已經在上一篇文章 詳解定時任務中的 CRON 表示式[1] 中詳細介紹,這裡不再贅述。
3.2 fixedDelay
fixedDelay。它的間隔時間是根據上次的任務結束的時候開始計時的,只要盯緊上一次執行結束的時間即可,跟任務邏輯的執行時間無關,兩個輪次的間隔距離是固定的。
3.3 fixedRate
fixedRate。這個相對難以理解一些。在理想情況下,下一次開始和上一次開始之間的時間間隔是一定的。但是預設情況下 Spring Boot 定時任務是單執行緒執行的
3.4 initialDelay
-
initialDelay 初始化延遲時間,也就是第一次延遲執行的時間。這個引數對
cron
屬性無效,只能配合fixedDelay
或fixedRate
使用。如@Scheduled(initialDelay=5000,fixedDelay = 1000)
表示第一次延遲5000
毫秒執行,下一次任務在上一次任務結束後1000
毫秒後執行。
4. Spring Task 的弊端
Spring Task 在實際應用中如果不明白一些機制會出現一些問題的,所以下面的一些要點十分重要。
4.1 單執行緒阻塞執行
從 3.3 章節 我們知道 Spring 的定時任務預設是單執行緒執行,多工情況下,如果使用多執行緒會影響定時策略。我們來演示一下:
package cn.felord.schedule.task;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* The type Task service.
*
* @author felord.cn
* @since 11 :02
*/
@Component
public class TaskService {
/**
* 上一次任務結束後 1 秒,執行下一次任務,任務消耗 5秒
*
* @throws InterruptedException the interrupted exception
*/
@Scheduled(fixedDelay = 1000)
public void task() throws InterruptedException {
System.out.println("Thread Name : "
+ Thread.currentThread().getName()
+ " i am a task : date -> "
+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
Thread.sleep(5000);
}
/**
* 下輪任務在本輪任務開始2秒後執行. 執行時間可忽略不計
*/
@Scheduled(fixedRate = 2000)
public void task2() {
System.out.println("Thread Name : "
+ Thread.currentThread().getName()
+ " i am a task2 : date -> "
+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}
}
上面定義了兩個定時任務(策略參見注釋),執行結果如下:
Thread Name : scheduling-1 i am a task2 : date -> 2020-01-13 17:16:19
Thread Name : scheduling-1 i am a task : date -> 2020-01-13 17:16:19
Thread Name : scheduling-1 i am a task2 : date -> 2020-01-13 17:16:24
Thread Name : scheduling-1 i am a task2 : date -> 2020-01-13 17:16:24
Thread Name : scheduling-1 i am a task2 : date -> 2020-01-13 17:16:25
Thread Name : scheduling-1 i am a task : date -> 2020-01-13 17:16:25
轉換為圖形比較好理解上面日誌的原因:
也就是說因為單執行緒阻塞發生了“連鎖反應”,導致了任務執行的錯亂。如果你準備用定時任務打算開啟 “11.11” 活動,豈不是背鍋的節奏。為了不背鍋我們就需要改造定時任務的機制。@EnableScheduling
註解引入了 ScheduledAnnotationBeanPostProcessor
其 setScheduler(Object scheduler)
有以下的註釋:
如果
TaskScheduler
或者ScheduledExecutorService
沒有定義為該方法的引數,該方法將在 Spring IoC 中尋找唯一的TaskScheduler
或者 名稱為taskScheduler
的 Bean 作為引數,當然你按照查詢TaskScheduler
的方法找一個ScheduledExecutorService
也可以。要是都找不到那麼只能使用本地單執行緒排程器了。
Spring Task 的呼叫順序關係為:任務排程執行緒 排程 任務執行執行緒 執行 定時任務 所以我們按照上面定義一個 TaskScheduler
在 Spring Boot 自動配置中提供了 TaskScheduler
的自動配置:
@ConditionalOnClass(ThreadPoolTaskScheduler.class)
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(TaskSchedulingProperties.class)
@AutoConfigureAfter(TaskExecutionAutoConfiguration.class)
public class TaskSchedulingAutoConfiguration {
@Bean
@ConditionalOnBean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)
@ConditionalOnMissingBean({ SchedulingConfigurer.class, TaskScheduler.class, ScheduledExecutorService.class })
public ThreadPoolTaskScheduler taskScheduler(TaskSchedulerBuilder builder) {
return builder.build();
}
@Bean
@ConditionalOnMissingBean
public TaskSchedulerBuilder taskSchedulerBuilder(TaskSchedulingProperties properties,
ObjectProvider<TaskSchedulerCustomizer> taskSchedulerCustomizers) {
TaskSchedulerBuilder builder = new TaskSchedulerBuilder();
builder = builder.poolSize(properties.getPool().getSize());
Shutdown shutdown = properties.getShutdown();
builder = builder.awaitTermination(shutdown.isAwaitTermination());
builder = builder.awaitTerminationPeriod(shutdown.getAwaitTerminationPeriod());
builder = builder.threadNamePrefix(properties.getThreadNamePrefix());
builder = builder.customizers(taskSchedulerCustomizers);
return builder;
}
}
該配置的自定義配置以 spring.task.scheduling
開頭。同時它需要在任務執行器配置 TaskExecutionAutoConfiguration
配置後才生效。我們只需要在中對其配置屬性 spring.task.execution
相關屬性配置即可。
Spring Boot 的 application.properties
中相關的配置說明:
# 任務排程執行緒池
# 任務排程執行緒池大小 預設 1 建議根據任務加大
spring.task.scheduling.pool.size=1
# 排程執行緒名稱字首 預設 scheduling-
spring.task.scheduling.thread-name-prefix=scheduling-
# 執行緒池關閉時等待所有任務完成
spring.task.scheduling.shutdown.await-termination=
# 排程執行緒關閉前最大等待時間,確保最後一定關閉
spring.task.scheduling.shutdown.await-termination-period=
# 任務執行執行緒池配置
# 是否允許核心執行緒超時。這樣可以動態增加和縮小執行緒池
spring.task.execution.pool.allow-core-thread-timeout=true
# 核心執行緒池大小 預設 8
spring.task.execution.pool.core-size=8
# 執行緒空閒等待時間 預設 60s
spring.task.execution.pool.keep-alive=60s
# 執行緒池最大數 根據任務定製
spring.task.execution.pool.max-size=
# 執行緒池 佇列容量大小
spring.task.execution.pool.queue-capacity=
# 執行緒池關閉時等待所有任務完成
spring.task.execution.shutdown.await-termination=true
# 執行執行緒關閉前最大等待時間,確保最後一定關閉
spring.task.execution.shutdown.await-termination-period=
# 執行緒名稱字首
spring.task.execution.thread-name-prefix=task-
配置完後你就會發現定時任務可以並行非同步執行了。
4.2 預設不支援分散式
Spring Task 並不是為分散式環境設計的,在分散式環境下,這種定時任務是不支援叢集配置的,如果部署到多個節點上,各個節點之間並沒有任何協調通訊機制,叢集的節點之間是不會共享任務資訊的,每個節點上的任務都會按時執行,導致任務的重複執行。我們可以使用支援分散式的定時任務排程框架,比如 Quartz、XXL-Job、Elastic Job。當然你可以藉助 zookeeper 、 redis 等實現分散式鎖來處理各個節點的協調問題。或者把所有的定時任務抽成單獨的服務單獨部署。
5. 總結
今天我們對 Spring Task 在 Spring Boot 中的應用進行簡單的瞭解。分析了定時任務的策略機制、對多工序列引發的問題的分析以及如何使得多工並行非同步執行。還對分散式下定時任務的一些常用解決方案進行了列舉。希望對你在使用 Spring Task 的過程中有所幫助, 原創技術乾貨請認準:felord.cn[2] 。