1. 程式人生 > 其它 >Spring Task定時任務

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 註解中一定要宣告定時任務的執行策略 cronfixedDelayfixedRate 三選一。

我們來認識一下 @Scheduled 提供了四個屬性。

3.1 cron 表示式

cron。這個我們已經在上一篇文章 詳解定時任務中的 CRON 表示式[1] 中詳細介紹,這裡不再贅述。

3.2 fixedDelay

fixedDelay。它的間隔時間是根據上次的任務結束的時候開始計時的,只要盯緊上一次執行結束的時間即可,跟任務邏輯的執行時間無關,兩個輪次的間隔距離是固定的。

3.3 fixedRate

fixedRate。這個相對難以理解一些。在理想情況下,下一次開始和上一次開始之間的時間間隔是一定的。但是預設情況下 Spring Boot 定時任務是單執行緒執行的

。當下一輪的任務滿足時間策略後任務就會加入佇列,也就是說當本次任務開始執行時下一次任務的時間就已經確定了,由於本次任務的“超時”執行,下一次任務的等待時間就會被壓縮甚至阻塞,算了畫張圖就明白了。

3.4 initialDelay

  • initialDelay 初始化延遲時間,也就是第一次延遲執行的時間。這個引數對 cron 屬性無效,只能配合 fixedDelayfixedRate 使用。如 @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 註解引入了 ScheduledAnnotationBeanPostProcessorsetScheduler(Object scheduler) 有以下的註釋:

如果 TaskScheduler 或者 ScheduledExecutorService 沒有定義為該方法的引數,該方法將在 Spring IoC 中尋找唯一的 TaskScheduler 或者 名稱為 taskSchedulerBean 作為引數,當然你按照查詢 TaskScheduler 的方法找一個ScheduledExecutorService 也可以。要是都找不到那麼只能使用本地單執行緒排程器了。

Spring Task 的呼叫順序關係為:任務排程執行緒 排程 任務執行執行緒 執行 定時任務 所以我們按照上面定義一個 TaskSchedulerSpring 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 Bootapplication.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。當然你可以藉助 zookeeperredis 等實現分散式鎖來處理各個節點的協調問題。或者把所有的定時任務抽成單獨的服務單獨部署。

5. 總結

今天我們對 Spring Task 在 Spring Boot 中的應用進行簡單的瞭解。分析了定時任務的策略機制、對多工序列引發的問題的分析以及如何使得多工並行非同步執行。還對分散式下定時任務的一些常用解決方案進行了列舉。希望對你在使用 Spring Task 的過程中有所幫助, 原創技術乾貨請認準:felord.cn[2]