確保Spring Boot定時任務只執行一次方案
環境:Spring Boot3.2.5
1. 簡介
在本篇文章中,我們將學習如何控制定時任務僅運行一次。定時任務是自動化諸如報告生成或發送通知等過程的常見做法。通常,我們設置這些任務定期運行。然而,在某些情況下,我們可能希望一個任務在未來的某個時間點僅執行一次,例如初始化資源或進行數據遷移等操作。
接下來將探討在Spring Boot應用中如何控制定時任務僅運行一次的幾種方法。本文將通過帶有初始延遲的@Scheduled注解,到更靈活的方法,如TaskScheduler和自定義觸發器來避免任務的重復執行。
2. 實戰案例
2.1 僅指定開始時間
雖然@Scheduled注解提供了一種直接的任務調度方式,但在靈活性方面做的不夠好。當需要對任務規劃進行更多控制(比如針對一次性執行的任務)時,Spring的TaskScheduler接口提供了多功能的替代方案。使用TaskScheduler,我們可以以編程方式安排任務,并指定任務的開始時間,從而為動態調度場景提供了更大的靈活性。
TaskScheduler中最簡單的方法允許我們定義一個Runnable任務和一個Instant,表示我們希望任務執行的具體時間。這種方法使我們能夠動態地安排任務,而不必依賴于固定的注解,如下示例:
public class TaskComponent {
private TaskScheduler taskScheduler = new SimpleAsyncTaskScheduler() ;
public void schedule(Runnable task, Instant startTime) {
taskScheduler.schedule(task, startTime) ;
}
}
TaskScheduler中的所有其他方法都是用于周期性執行的,因此這個方法對于一次性任務很有幫助。
注:這里我們使用了SimpleAsyncTaskScheduler,如果你的環境是Spring6.1以上版本,那么你還可以通過設置為虛擬線程執行任務。
SimpleAsyncTaskScheduler scheduler = new SimpleAsyncTaskScheduler();
scheduler.setVirtualThreads(true) ;
接下來,我們可以通過如下方式測試該種方式的定時任務執行
@Test
public void testOnceTask() {
CountDownLatch cdl = new CountDownLatch(1) ;
scheduler.schedule(() -> {
cdl.countDown();
// TODO, 任務Action
}, Instant.now().plus(Duration.ofSeconds(1))) ;
latch.await() ;
}
這里僅僅是因為通過單元測試時需要通過CountDownLatch進行協助測試。生產環境無需這樣。
2.2 通過@Scheduled方式
我們還可以使用@Scheduled注解并設置初始延遲時間,同時不要設置fixedDelay或fixedRate屬性,如下示例:
@Component
public class TaskComponent {
@Scheduled(initialDelay = 3000)
public void task() {
// TODO, 任務Action
}
}
該方法在指定的初始化3s后執行任務之后不會重復執行。
2.3 自定義觸發器
我們還可以通過實現PeriodicTrigger,使用PeriodicTrigger我們可以進行更多的控制任務執行。通過重寫nextExecution()方法,以便僅在我們尚未觸發的情況下返回下一個執行時間,如下示例:
public class PackTrigger extends PeriodicTrigger {
public PackTrigger(Instant when) {
super(Duration.ofSeconds(1)) ;
Duration difference = Duration.between(Instant.now(), when) ;
setInitialDelay(difference) ;
}
@Override
public Instant nextExecution(TriggerContext triggerContext) {
if (triggerContext.lastCompletion() == null) {
return super.nextExecution(triggerContext) ;
}
// 返回null后將不會再執行
return null;
}
}
由于我們只希望它執行一次,因此可以將周期設置為任意值。最終,需要為我們的初始延遲傳遞一個Duration,因此我們需要計算任務期望執行時刻與當前時間之間的差值。
重寫nextExecution方法,如果完成狀態為null,則表示它尚未觸發,因此允許它調用默認實現。否則,返回null,這將使此觸發器僅執行一次,調用示例如下:
TaskScheduler taskScheduler = new SimpleAsyncTaskScheduler() ;
taskScheduler.schedule(() -> {
System.out.printf("%s, 執行任務%n", Thread.currentThread().getName()) ;
}, new PackTrigger(Instant.now().plusSeconds(2))) ;
通過此種自定義觸發器的方式更加的靈活控制定時任務的執行,也是當前最優的選擇。