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@EnableSchedulingpublic 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 **/@Componentpublic 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 */@Componentpublic 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
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# 核心線程池大小 默認 8spring.task.execution.pool.core-size=8# 線程空閒等待時間 默認 60sspring.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] 。
轉載自:https://blog.csdn.net/qq_35067322/article/details/103982304
閱讀更多 技術宅愛Java 的文章