理解Spring定時任務的fixedRate和fixedDelay


  用過  Spring 的 @EnableScheduling 的都知道,我們用三種形式來部署計划任務,即 @Scheduled 注解的 fixedRate(fixedRateString), fixedDelay(fixedDelayString), 以及 cron. cron 不在這里討論的范疇。

  我們着重在如何理解 fixedRate 和 fixedDelay 的區別。

  在 Spring 的  Scheduled 注解的 JavaDoc 對此的解釋很簡單

public abstract long fixedRate 
Execute the annotated method with a fixed period in milliseconds between invocations.

public abstract long fixedDelay 
Execute the annotated method with a fixed period in milliseconds between the end of the last invocation and the start of the next.

  只是說是 fixedRate 任務兩次執行時間間隔是任務的開始點,而 fixedDelay 的間隔是前次任務的結束與下次任務的開始。

  大致用示意字符串來表示如下(每個 T1, 或 T2 代表任務執行秒數(每次任務執行時間不定),假定 fixedRate 或  fixedDelay 的值是 5 秒,用 W 表示等待的數)

 

  fixedRate:    T1.T1WWWT2.T2.T2WW.T3.T3.T3.T3.T3.T4.T4.T4.T4.T4.T4.T4T5T5WWWT6.T6........

 

  fixedDelay:  T1.T1.WWWWW.T2.T2.T2WWWWW.T3.T3.T3.T3.T3.WWWWW.T4.T4.T4.T4.T4.T4.T4.WWWWWT6.T6......

  一般來說能理解到上面兩個場景已經差不多了,相比而言 fixedDelay 簡單些,盯着上一次任務的屁股就行。

 

  以前我對 fixedRate 還有一個誤區就是,以為任務時長超過 fixedRate 時會啟動多個任務實例,其實不會; 只不過會在上次任務執行完后立即啟動下一輪。除非這個 Job 方法用 @Async 注解了,使得任務不在 TaskScheduler 線程池中執行,而是每次創建新線程來執行。

  具體理解我們可以用代碼來演示

@EnableScheduling
@SpringBootApplication
public class Application {
 
    private AtomicInteger number = new AtomicInteger();
 
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
 
    @Bean
    public TaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
        taskScheduler.setPoolSize(5);
        return taskScheduler;
    }
 
    @Scheduled(fixedRate = 5000)
    public void job() {
        LocalTime start = LocalTime.now();
        System.out.println(Thread.currentThread() + " start " + number.incrementAndGet() + " @ "  + start);
        try {
            Thread.sleep(ThreadLocalRandom.current().nextInt(15) * 1000);
        } catch (InterruptedException e) {
        }
        LocalTime end = LocalTime.now();
        System.out.println(Thread.currentThread() + " end " + number.get() + " @ " + end
            + ", seconds cost " + (ChronoUnit.SECONDS.between(start, end)));
    }
}

  初始化了一個線程池大小為 5  的 TaskScheduler, 避免了所有任務都用一個線程來執行。 上例中的 fixedRate 為 5 秒,任務執行時間在 0 ~ 15 秒之間,先來看一組數據(樣本數據越多越生動)

Thread[taskScheduler-1,5,main] start 1 @ 01:23:11.726
Thread[taskScheduler-1,5,main] end 1 @ 01:23:24.732, seconds cost 13
Thread[taskScheduler-1,5,main] start 2 @ 01:23:24.736
Thread[taskScheduler-1,5,main] end 2 @ 01:23:28.737, seconds cost 4
Thread[taskScheduler-2,5,main] start 3 @ 01:23:28.738
Thread[taskScheduler-2,5,main] end 3 @ 01:23:40.739, seconds cost 12
Thread[taskScheduler-1,5,main] start 4 @ 01:23:40.740
Thread[taskScheduler-1,5,main] end 4 @ 01:23:52.745, seconds cost 12
Thread[taskScheduler-3,5,main] start 5 @ 01:23:52.745
Thread[taskScheduler-3,5,main] end 5 @ 01:24:00.748, seconds cost 8
Thread[taskScheduler-3,5,main] start 6 @ 01:24:00.749
Thread[taskScheduler-3,5,main] end 6 @ 01:24:05.750, seconds cost 5
Thread[taskScheduler-3,5,main] start 7 @ 01:24:05.750
Thread[taskScheduler-3,5,main] end 7 @ 01:24:05.750, seconds cost 0
Thread[taskScheduler-3,5,main] start 8 @ 01:24:05.750
Thread[taskScheduler-3,5,main] end 8 @ 01:24:14.752, seconds cost 9
Thread[taskScheduler-3,5,main] start 9 @ 01:24:14.752
Thread[taskScheduler-3,5,main] end 9 @ 01:24:26.756, seconds cost 12
Thread[taskScheduler-3,5,main] start 10 @ 01:24:26.757
Thread[taskScheduler-3,5,main] end 10 @ 01:24:39.757, seconds cost 13
Thread[taskScheduler-3,5,main] start 11 @ 01:24:39.757
Thread[taskScheduler-3,5,main] end 11 @ 01:24:43.761, seconds cost 4
Thread[taskScheduler-3,5,main] start 12 @ 01:24:43.762
Thread[taskScheduler-3,5,main] end 12 @ 01:24:47.763, seconds cost 4
Thread[taskScheduler-3,5,main] start 13 @ 01:24:47.763
Thread[taskScheduler-3,5,main] end 13 @ 01:24:49.766, seconds cost 2
Thread[taskScheduler-3,5,main] start 14 @ 01:24:49.767

把 start 行用紅色顯示。

  1. 任務 1 與 2 之間間隔時間是任務時長 13,所以任務 2 在 1 結束后立即啟動
  2. 任務 3 與 2 之間間隔還不到 5 秒,也是在任務 2 結束后立即執行
  3. 后面都是在上次任務結束后立即執行下一次任務,看到 7 與 8 之間相差 0 秒,13 與 14 之間相關 2 秒

從上面的結果分析,似乎 fixedRate 越到后面都不起作用,總是任務一個接一個的執行。也就是說上面 fixedRate 的示意串

T1.T1WWWT2.T2.T2WW.T3.T3.T3.T3.T3.T4.T4.T4.T4.T4.T4.T4T5T5WWWT6.T6........

已經不成立了,當中間發生了一長時間的任務后,fixedRate 變成了如下的形式

T1.T1.WWWT2.T2.T2.T2.T2.T2.T2.T2.T2.T2.T2.T2.T3.T3.T3.T3.T4.T4.T4.T5.T5.T5.......

  任務間的等待都被抹除掉了,這是為什么呢?因為 fixedRate 會對將要執行的任務作一個預先編排,由上輸出可以第一次任務在 01:23:11 時間點啟動,所以  fixedRate 會基於此把一個時間表准備好,如下

01:23:16 T2 T1 執行后時間來到了 01:23:24, 下一次任務 T2 安排在更早的時間,所以立即執行 T2
01:23:21 T3 T2 完后時間是 01:23:28, T3 的安排時間也比它早,所以也是立即執行 T3
01:23:26 T4 T3 完后時間是 01:23:40, 無需等待立即執行 T4
01:23:31 T5

后面的情況都是一樣的, T5.endTime > T6.scheduledTime + fixedRate, 所以立即執行 T6 

除非有一些短任務能把時間壓縮回去,造成上一次任務結束后需要進行等待

01:23:35 T6
01:23:41 T7

   因此,fixedRate 總是在上一次任務結束后從時間表中挑出下一次任務,對比該任務所預先排好的時間是否晚於上次任務啟動時間加上 fixedRate 值,是則等待到預定的時間,否則立即執行。

  假設 T1 執行完后時間是 T1.endTime, 這時候判斷 T1.endTime < T2.scheduledTime + fixedRate,  是則等待到 T2.scheduledTime 啟動 T2, 否則立即執行  T2

  我們可以用代碼進一步來驗證上面的說法,其實最具說服力的莫過於源代碼,這里只提供感觀體驗

  代碼的改動是第一次任務執行時間為 23  秒,此后的任務是不耗時的空操作

 private AtomicBoolean firstTime = new AtomicBoolean(true);
 
    @Scheduled(fixedRate = 5000)
    public void job() {
        LocalTime start = LocalTime.now();
        System.out.println(Thread.currentThread() + " start " + number.incrementAndGet() + " @ "  + start);
        if (firstTime.getAndSet(false)) {
            try {
                Thread.sleep(23000);
            } catch (InterruptedException e) {
            }
        }
        LocalTime end = LocalTime.now();
        System.out.println(Thread.currentThread() + " end " + number.get() + " @ " + end
            + ", seconds cost " + (ChronoUnit.SECONDS.between(start, end)));
    }

  輸出為

Thread[taskScheduler-1,5,main] start 1 @ 03:27:54.556
Thread[taskScheduler-1,5,main] end 1 @ 03:28:17.562, seconds cost 23
Thread[taskScheduler-1,5,main] start 2 @ 03:28:17.566
Thread[taskScheduler-1,5,main] end 2 @ 03:28:17.566, seconds cost 0
Thread[taskScheduler-2,5,main] start 3 @ 03:28:17.566
Thread[taskScheduler-2,5,main] end 3 @ 03:28:17.567, seconds cost 0
Thread[taskScheduler-1,5,main] start 4 @ 03:28:17.584
Thread[taskScheduler-1,5,main] end 4 @ 03:28:17.584, seconds cost 0
Thread[taskScheduler-4,5,main] start 5 @ 03:28:17.584
Thread[taskScheduler-4,5,main] end 5 @ 03:28:17.584, seconds cost 0
Thread[taskScheduler-4,5,main] start 6 @ 03:28:19.549
Thread[taskScheduler-4,5,main] end 6 @ 03:28:19.550, seconds cost 0
Thread[taskScheduler-4,5,main] start 7 @ 03:28:24.549
Thread[taskScheduler-4,5,main] end 7 @ 03:28:24.550, seconds cost 0
Thread[taskScheduler-4,5,main] start 8 @ 03:28:29.548
Thread[taskScheduler-4,5,main] end 8 @ 03:28:29.549, seconds cost 0
Thread[taskScheduler-4,5,main] start 9 @ 03:28:34.546

  因為第一次任務 23 秒的延誤,所以后續的任務 2, 3, 4, 5 都是上次任務(耗時為 0)完后立即執行,任務 6 把 2 秒的差距找回來了,以后都是每隔 5 秒執行一次。

  fixedDelay 的邏輯就相當簡單了,基本無需用代碼來演示。不妨把上面的代碼中的 fixedRate 改成 fixedDelay 來一見分曉:

Thread[taskScheduler-1,5,main] start 1 @ 02:54:33.750
Thread[taskScheduler-1,5,main] end 1 @ 02:54:43.756, seconds cost 10
Thread[taskScheduler-1,5,main] start 2 @ 02:54:48.765
Thread[taskScheduler-1,5,main] end 2 @ 02:55:00.767, seconds cost 12
Thread[taskScheduler-2,5,main] start 3 @ 02:55:05.769
Thread[taskScheduler-2,5,main] end 3 @ 02:55:11.772, seconds cost 6
Thread[taskScheduler-1,5,main] start 4 @ 02:55:16.775
Thread[taskScheduler-1,5,main] end 4 @ 02:55:21.781, seconds cost 5
Thread[taskScheduler-3,5,main] start 5 @ 02:55:26.785
Thread[taskScheduler-3,5,main] end 5 @ 02:55:27.787, seconds cost 1
Thread[taskScheduler-3,5,main] start 6 @ 02:55:32.789
Thread[taskScheduler-3,5,main] end 6 @ 02:55:41.792, seconds cost 9
Thread[taskScheduler-3,5,main] start 7 @ 02:55:46.794

  總是上次任務結束 5 秒后,由此可見 fixedDelay 不存在任務的預先編排操作了,都是相機而為。

  最后小結一下:fixedRate 每次任務結束后會從任務編排表中找下一次該執行的任務,判斷是否到時機執行。fixedRate 的任務某次執行時間再長也不會造成兩次任務實例同時執行,除非用了 @Async 注解。 fixedDelay 總是前一次任務完成后,延時固定長度然后執行一次任務

 

  本文來自於: https://unmi.cc/understand-spring-schedule-fixedrate-fixeddelay/, 來自 隔葉黃鶯 Unmi Blog

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM