玩轉SpringBoot:SpringBoot的幾種定時任務實現方式

码农Academy發表於2024-03-07

引言

在現代軟體開發中,定時任務是一種常見的需求,用於執行週期性的任務或在特定的時間點執行任務。這些任務可能涉及資料同步、資料備份、報表生成、快取重新整理等方面,對系統的穩定性和可靠性有著重要的影響。Spring Boot提供了強大且簡單的定時任務功能,使開發人員能夠輕鬆地管理和執行這些任務。

本文將介紹 Spring Boot中定時任務的基本用法、高階特性以及最佳實踐,幫助開發人員更好地理解和應用定時任務,提高系統的穩定性和可靠性。

SpringBoot中的定時任務

SpringBoot中的定時任務主要透過@Scheduled註解以及SchedulingConfigurer介面實現。

@Scheduled註解

@Scheduled註解是Spring提供的一個註解,用於標記方法作為定時任務執行。透過 @Scheduled註解,開發人員可以輕鬆地配置方法在指定的時間間隔或時間點執行,實現各種定時任務需求。

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})  
@Retention(RetentionPolicy.RUNTIME)  
@Documented  
@Repeatable(Schedules.class)  
public @interface Scheduled {

	String cron() default "";

	long fixedDelay() default -1;

	long fixedRate() default -1;

	long initialDelay() default -1;
}

以上為@Scheduled原始碼中關鍵屬性,各屬性含義如下:

  • cron: 接受標準的Unix Cron表示式,用於定義複雜的計劃執行時間。
/**  
* cron屬性可以設定指定時間執行,cron表示式跟linux一樣  
*/  
@Scheduled(cron = "0 45 14 ? * *")  
public void fixTimeExecution() {  
	System.out.println("指定時間 "+dateFormat.format(new Date())+"執行");  
}
  • fixedRate: 以固定的頻率執行任務,指定兩次執行之間的間隔時間(單位是毫秒)。
/**  
* fixedRate屬性設定每隔固定時間執行  
*/  
@Scheduled(fixedRate = 5000)  
public void reportCurrentTime() {  
	System.out.println("每隔五秒執行一次" + dateFormat.format(new Date())); 
}
  • fixedDelay:在每次任務完成後等待一定的時間再進行下一次執行,指定連續執行之間的延遲時間。
/**  
* 上一次任務執行完成之後10秒後在執行  
*/  
@Scheduled(fixedDelay = 10000)  
public void runWithFixedDelay() {  
	System.out.println("指定時間 "+dateFormat.format(new Date())+"執行");  
}
  • initialDelay:首次執行前的延遲時間。
/**  
* 初始延遲1秒後開始,然後每10秒執行一次  
*/  
@Scheduled(initialDelay=1000, fixedDelay=10000)  
public void executeWithInitialAndFixedDelay() {  
	System.out.println("指定時間 "+dateFormat.format(new Date())+"執行");  
}

這裡要注意fixedRate與fixedDelay的區別:fixedRate是基於任務開始執行的時間點來計算下一次任務開始執行的時間,因此任務的執行時間間隔是相對固定的,不受到任務執行時間的影響。如果指定的時間間隔小於任務執行的實際時間,則任務可能會併發執行。而fixedDelay是基於任務執行完成的時間點來計算下一次任務開始執行的時間,因此任務的執行時間間隔是相對不規則的,受到任務執行時間的影響。

SpringBoot支援同時定義多個定時任務方法,每個方法可以使用不同的引數配置,以滿足不同的定時任務需求。同時,我們必須在配置類中使用@EnableScheduling註解開啟定時任務。

@Configuration  
@EnableScheduling  
public class ScheduledTaskConfig { 

}

或者

@EnableScheduling  
@SpringBootApplication  
public class SpringBootBaseApplication {  
  
    public static void main(String[] args) {  
       SpringApplication.run(SpringBootBaseApplication.class, args);  
    }  
}

在SpringBoot應用程式中,除了在程式碼中使用註解配置定時任務外,還可以透過配置檔案來配置定時任務的執行規則。這種方式更加靈活,可以在不修改原始碼的情況下,動態調整定時任務的執行規則。比如我們在application.properties中配置@Scheduled的屬性:

custom.scheduled.cron = 0/5 * * * * ?  
custom.scheduled.fixedRate=5000  
custom.scheduled.fixedDelay=10000  
custom.scheduled.initialDelay=1000

然後在@Scheduled的方法使用屬性配置定時任務執行頻率。

@Service  
public class DemoScheduledTaskService {  
  
    private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");  
  
    /**  
     * fixedRate屬性設定每隔固定時間執行  
     */  
    @Scheduled(fixedRateString = "${custom.scheduled.fixedRate}")  
    public void reportCurrentTime() {  
        System.out.println("每隔五秒執行一次" + dateFormat.format(new Date()));  
    }  
  
    /**  
     * cron屬性可以設定指定時間執行,cron表示式跟linux一樣  
     */  
    @Scheduled(cron = "${custom.scheduled.cron}")  
    public void fixTimeExecution() {  
        System.out.println("指定時間 "+dateFormat.format(new Date())+"執行");  
    }  
  
    /**  
     * 上一次任務執行完成之後10秒後在執行  
     */  
    @Scheduled(fixedDelayString = "${custom.scheduled.fixedDelay}")  
    public void runWithFixedDelay() {  
        System.out.println("指定時間 "+dateFormat.format(new Date())+"執行");  
    }  
  
    /**  
     * 初始延遲1秒後開始,然後每10秒執行一次  
     */  
    @Scheduled(initialDelayString = "${custom.scheduled.initialDelay}", fixedDelayString = "${custom.scheduled.fixedDelay}")  
    public void executeWithInitialAndFixedDelay() {  
        System.out.println("指定時間 "+dateFormat.format(new Date())+"執行");  
    }  
}

注意,這裡使用屬性來指定任務執行頻率時,要透過@Scheduled的fixedRateString、fixedDelayString、initialDelayString三個可以指定字串的值的屬性去指定,效果等同於long型別的屬性。

透過配置檔案配置定時任務具有很高的靈活性,可以在不重新編譯和部署應用程式的情況下,隨時調整定時任務的執行規則。同時,也可以根據不同的環境(例如開發、測試、生產)配置不同的定時任務規則,以滿足不同環境下的需求。這種方式可以有效地解耦定時任務的配置和業務程式碼,提高系統的靈活性和可維護性。

另外,如果希望定時任務能夠非同步執行,不阻塞主執行緒,可以在方法上同時加上@Async註解,這樣各任務就可以非同步執行了。有關SpringBoot中使用@Async的講解,請移步:

雖然 @Scheduled 註解是一個方便的方式來定義定時任務,但它也存在一些弊端。因為任務的執行計劃(如cron表示式)在編譯時被硬編碼,因此無法在執行時動態修改,除非重新部署。此外,@Scheduled註解對於配置不同的排程策略(如使用不同的執行緒池)顯得力不從心,而且預設情況下,@Scheduled任務在單執行緒環境下執行,可能出現任務堆積的情況,尤其在任務量大或任務執行時間長的情況下,而且這些任務可能會變得混亂和難以管理。定時任務的配置分散在各個任務方法中,不利於統一管理和維護。對於需要根據動態條件建立或銷燬定時任務的情況,@Scheduled註解也無法滿足需求。

為了解決這些問題,可以使用SchedulingConfigurer介面來動態地建立和管理定時任務。透過實現 SchedulingConfigurer 介面,我們可以編寫程式碼來動態地註冊和管理定時任務,從而實現靈活的任務排程需求。接下來,我們將介紹如何使用SchedulingConfigurer介面來建立定時任務。

SchedulingConfigurer介面

SchedulingConfigurer 介面是 Spring 提供的一個用於定時任務配置的擴充套件介面,它允許開發人員更細粒度地控制定時任務的執行。透過實現SchedulingConfigurer介面,可以自定義任務排程器(TaskScheduler),配置執行緒池等引數,以滿足不同場景下的定時任務需求。

@Configuration  
@EnableScheduling  
public class CustomSchedulingConfig implements SchedulingConfigurer {

	@Override  
	public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
		// 定時任務邏輯
	}
}

透過實現SchedulingConfigurer介面,重寫configureTasks方法,自定義任務排程器的配置。此外我們還可以配置執行緒池,用於控制定時任務執行時的執行緒數量、併發性等引數。

@Bean(destroyMethod = "shutdown")  
public ThreadPoolTaskScheduler threadPoolTaskScheduler() {  
    ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();  
    scheduler.setPoolSize(5); // 設定執行緒池大小  
    scheduler.setThreadNamePrefix("scheduled-task-"); // 設定執行緒名稱字首  
    scheduler.setAwaitTerminationSeconds(60); // 設定終止等待時間  
	// 設定處理拒絕執行的任務異常
	scheduler.setRejectedExecutionHandler((r, executor) -> log.error("Task rejected", r));
	// 處理定時任務執行過程中丟擲的未捕獲異常
	scheduler.setErrorHandler(e -> log.error("Error in scheduled task", e));
    return scheduler;  
}

然後將自定義的ThreadPoolTaskScheduler設定到ScheduledTaskRegistrar中去:

@Override  
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
	// 定時任務邏輯
	taskRegistrar.setTaskScheduler(threadPoolTaskScheduler());
}

有關執行緒池的配置引數講解,請移步:

透過SchedulingConfigurer介面,可以更靈活地配置任務排程器和定時任務的執行規則,比如動態註冊定時任務、動態修改任務執行規則等。

  • 動態新增定時任務
    SchedulingConfigurerconfigureTasks方法中,我們可以根據業務需求,從資料庫、配置檔案或其它動態來源獲取定時任務的資訊(如Cron表示式、任務執行類等),然後建立對應的RunnableCallable例項,並結合Trigger(如CronTrigger)將其新增到排程器中。相比@Scheduled註解,這種方式能夠在應用執行時隨時新增新的定時任務。
@Override  
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {  
    ThreadPoolTaskScheduler scheduler = threadPoolTaskScheduler();  
    taskRegistrar.setTaskScheduler(scheduler);  
  
    List<CronTaskInfo> tasksFromDB = listTasksFromDatabase();  
  
    for (CronTaskInfo task : tasksFromDB) {  
        Runnable taskRunner = new MyTaskExecutor(task.getTaskData());  
        CronTrigger cronTrigger = new CronTrigger(task.getCronExpression());  
        scheduler.schedule(taskRunner, cronTrigger);  
    }  
}

關於這裡在應用執行時,動態的新增新的任務,我們可以透過事件驅動,輪訓檢查,訊息佇列等多種方式,監聽到資料庫中或者配置檔案中新增任務資訊,然後透過SchedulingConfigurer介面動態建立定時任務。而這種方式是@Scheduled註解做不到的。

  • 修改定時任務規則
    當任務的執行規則需要動態變更時,同樣可以在configureTasks方法中實現。例如,從資料庫獲取最新的Cron表示式,然後取消當前任務並重新新增新的任務例項。需要注意的是,取消已有任務通常需要持有對該任務的引用,例如使用Scheduler提供的unschedule方法。
// 假設我們有一個方法用於獲取更新後的任務資訊  
CronTaskInfo updatedTask = getUpdatedTaskInfoFromDatabase();  
  
// 取消舊的任務(需要知道舊任務的TriggerKey)  
TriggerKey triggerKey = ...; // 獲取舊任務的TriggerKey  
scheduler.unschedule(triggerKey);  
  
// 建立新任務並設定新的Cron表示式  
MyTaskExecutor taskExecutor = new MyTaskExecutor(updatedTask.getTaskData());  
CronTrigger updatedCronTrigger = new CronTrigger(updatedTask.getCronExpression());  
  
// 重新排程新任務  
scheduler.schedule(taskRunner, updatedCronTrigger);

另外,我們還可以透過新增任務時對其排序或設定優先順序等方式間接實現設定定時任務的執行順序。

透過實現SchedulingConfigurer介面,我們可以擁有對定時任務排程的更多控制權,比如自定義執行緒池、動態新增任務以及調整任務執行策略。這種靈活性使得在複雜環境下,特別是需要動態管理定時任務時,SchedulingConfigurer成為了理想的選擇。

其他第三方任務排程框架

除了使用Spring框架提供的 @Scheduled 註解和SchedulingConfigurer介面外,還有許多第三方的任務排程庫可供選擇。這些庫通常提供了更多的功能和靈活性,以滿足各種複雜的任務排程需求。以下是一些常見的第三方任務排程庫:

  1. Quartz Scheduler
    Quartz是一個功能強大且靈活的任務排程庫,具有豐富的功能,如支援基於cron表示式的任務排程、叢集支援、作業持久化等。它可以與Spring框架整合,並且被廣泛應用於各種型別的任務排程應用程式中。

  2. Elastic Job
    Elastic Job是一個分散式任務排程框架,可以輕鬆實現分散式任務排程和作業執行。它提供了分散式任務執行、作業依賴關係、作業分片等功能,適用於大規模的分散式任務排程場景。

  3. xxl-job
    xxl-job是一個分散式任務排程平臺,提供了視覺化的任務管理介面和多種任務排程方式,如單機任務、分散式任務、定時任務等。它支援任務執行日誌、任務失敗重試、動態調整任務執行策略等功能。

  4. PowerJob
    PowerJob是一個開源的分散式任務排程框架,由阿里巴巴集團開發並開源。PowerJob 提供了分散式、高可用的任務排程能力,支援多種任務型別,如定時任務、延時任務、流程任務等。

總結

定時任務在現代軟體開發中扮演著重要的角色,它們可以自動化執行各種重複性的任務,提高系統的效率和可靠性。SpringBoot提供了強大而靈活的定時任務功能,使我們能夠輕鬆地管理和執行各種定時任務。透過@Scheduled註解和SchedulingConfigurer介面,我們可以根據需求配置定時任務的執行規則,實現各種複雜的定時任務排程需求。我們可以充分利用SpringBoot中的定時任務功能,提高系統的穩定性和可靠性,從而更好地滿足業務需求。

本文已收錄於我的個人部落格:碼農Academy的部落格,專注分享Java技術乾貨,包括Java基礎、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中介軟體、架構設計、面試題、程式設計師攻略等

相關文章