Spring Boot 中使用 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@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。它的間隔時間是根據上次的任務結束的時候開始計時的,只要盯緊上一次執行結束的時間即可,跟任務邏輯的執行時間無關,兩個輪次的間隔距離是固定的。

Spring Boot 中使用 Spring Task 實現定時任務

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

轉換為圖形比較好理解上面日誌的原因:

Spring Boot 中使用 Spring Task 實現定時任務

也就是說因為單線程阻塞發生了“連鎖反應”,導致了任務執行的錯亂。如果你準備用定時任務打算開啟 “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


分享到:


相關文章: