定時任務莫名停止,Spring 定時任務存在 Bug?

java技術架構 發佈 2020-02-03T10:11:12+00:00

註解配置在上面問題排查中,我們知道Spring 將會查找 TaskScheduler/ScheduledExecutorService,若存在將會使用。

專注於Java領域優質,技術歡迎關注

作者: 鴨血粉絲 來自:Java極客技術

Hello~各位讀者新年好,我是鴨血粉絲(大家可以稱呼我為「阿粉」)。這裡阿粉給大家拜個年,祝大家蒸蒸日上燙燙燙,年年有餘屯屯屯。

那年那 Bug

春節放假,阿粉坐上高鐵回家,路上阿粉突然想到一次生產問題。那是阿粉參加工作第一年,那一年國慶假期,阿粉提前一天請假回家辦個護照。那時候阿粉剛開始負責的系統,所以工作日請假,還是有點擔心,就怕問題看阿粉不在,悄然上門。

哎,真實越怕什麼,就來什麼。


高鐵開到一半的時候,同事反饋系統不能獲取最新的流水信息(流水信息通過 Spring 定時任務定時拉取)。阿粉心裡一驚,立刻拔出電腦,連上 VPN,準備登上生產機器,查看系統情況。可是,高鐵上網絡大家也懂,很不穩定,連了好久連不上 VPN,只好遠程指揮同事看一下系統日誌。通過同事反饋的日誌,發現拉取流水定時任務沒有執行,進一步查看,阿粉發現整個系統其他的定時任務也都停止了。。。

這真是一個奇怪的的問題,這好端端的定時任務怎麼會突然停止?

暫時想不到解決辦法,只好指揮同事先重啟應用。重啟之後,暫時解決問題,定時任務重新開始執行,也獲取到最新的付款流水信息。

問題排查

到家之後,阿粉立刻登上生產機器,查看系統日誌。這裡阿粉發現重啟之前某一定時任務運行到一半,並且在這之後其他定時任務就沒有再被執行。

通過系統日誌,定位到了有問題的代碼。


這裡採用重試補償策略,防止查詢流水信息因為網絡等問題發生偶發的失敗。這個策略面對偶發的失敗沒什麼問題,但是如果查詢銀行流水服務一直失敗,這段代碼就會陷入死循環。恰巧那段時間網絡出現一些問題,導致這裡查詢一直處於失敗。

增加最大重試次數,修復該 Bug。


修復之後,立刻將最新版本代碼部署到生產系統,暫時解決了這個問題。

知識點:面對一些失敗,可以採用重試補償策略,重新執行,最大可能保證執行成功,但是這裡切記設置合適的的重大的次數

深入排查

雖然問題解決了,但是阿粉心裡還是存在一個疑惑,為何一個定時任務發生了阻塞,就會影響執行其他定時任務。阿粉最初的理解是不同的定時任務應該互相隔離,互不影響才對,真難到是 Spring 定時任務的 Bug 嗎?

想到這裡,阿粉決定寫一個 Demo,復現問題,然後深入源碼排查。


啟動程序,日誌輸出如下:


從日誌可以看到,fixDelayMethod 方法執行之後進入休眠,直到休眠結束,cronMethod 定時任務才有機會被執行。另外從上面可以看到,上述兩個定時任務都由 pool-1-thread-1線程執行。從這點可以看出 Spring 定時任務將會交給線程池執行。

知識點: 線程池中線程默認命名策略為 pool-%poolNumber-thread-%num。

如果線程池只有一個工作線程,該線程一旦被長時間阻塞,堆積的其他任務就沒有機會被執行。

那麼是不是這個問題導致的 Sping 定時任務停止執行?我們繼續往下排查。

圖上日誌綠色部分, ScheduledAnnotationBeanPostProcessor 輸出一個重要信息:

No TaskScheduler/ScheduledExecutorService bean found for scheduled processing

查看 Spring 文檔,Spring 內部將會通過調用 TaskScheduler 執行定時任務,而另一個 ScheduledExecutorService 為 JDK 提供執行定時任務的執行器。記住這兩者


通過這段日誌,使用 IDEA 的強大的關鍵字搜索功能,定位到 ScheduledAnnotationBeanPostProcessor#finishRegistration 方法。


這個方法比較長,大家重點關注圖中標示的幾處。

Spring 啟動之後將會掃描所有 Bean 中帶有 @Scheduled 註解的方法,然後封裝成 Task子類放置到 ScheduledTaskRegistrar。

這段代碼位於 ScheduledAnnotationBeanPostProcessor#processScheduled,感興趣的可以翻閱查看

如果此時 ScheduledTaskRegistrar 不存在定時任務或者 ScheduledTaskRegistrar 中的 TaskScheduler不存在,finishRegistration將會多次調用 ScheduledAnnotationBeanPostProcessor#resolveSchedulerBean 方法用以查找 TaskScheduler/ScheduledExecutorService。


接下去將會把獲取到 Bean 通過 setScheduler 注入到 ScheduledTaskRegistrar 中。


如果獲取的為 ScheduledExecutorService 類型,將會將其封裝到 taskScheduler中。

最後還沒找到,將會輸出最剛開始見到的日誌。然後 Spirng 將會在 ScheduledTaskRegistrar#afterPropertiesSet 創建一個單線程的定時任務執行器 ScheduledExecutorService,注入到 ConcurrentTaskScheduler中,然後通過 taskScheduler 執行定時任務。


image-20200125144040781

交給TaskScheduler 的定時任務最後實際上還是通過 ScheduledExecutorService執行。


這裡可以得出一個結論

Spring 定時任務實際上通過 JDK 提供的 ScheduledExecutorService執行。默認情況下,Spring 將會生成一個單線程ScheduledExecutorService執行定時任務。所以一旦某一個定時任務長時間阻塞這個執行線程,其他定時任務都將被影響,沒有機會被執行線程執行。

Spring 這種默認配置,在需要執行多個定時任務的情況,可能會是一個坑。我們可以通過改變配置,使 Spring 採用多線程執行定時任務。

自定義配置

Spring 可以通過多種方式改變默認配置。

xml 配置

通過 xml 配置 TaskScheduler 線程數。

<task:annotation-driven scheduler="myScheduler"/>

<task:scheduler id="myScheduler" pool-size="10"/>

通過上面的配置,Spring 將會使用 TaskScheduler 子類 ThreadPoolTaskScheduler,內部線程數為 pool-size 數量,這個線程數將會直接設置 ScheduledExecutorService 線程數量。

註解配置

在上面問題排查中,我們知道 Spring 將會查找 TaskScheduler/ScheduledExecutorService,若存在將會使用。所以這裡我們可以生成這些類的 Bean。


以上方式二選一即可

SpringBoot 配置

上面兩種配置適用於普通 Spring,比較繁瑣。相比而言 SpringBoot 配置將會非常簡單,只需要在啟動配置文件加入如下配置即可。

spring.task.scheduling.pool.size=10

spring.task.scheduling.thread-name-prefix=task-test

技術總結

下面開始技術總結:

  1. Spring 定時任務執行原理實際使用的是 JDK 自帶的 ScheduledExecutorService
  2. Spring 默認配置下,將會使用具有單線程的 ScheduledExecutorService
  3. 單線程執行定時任務,如果某一個定時任務執行時間較長,將會影響其他定時任務執行
  4. 如果存在多個定時任務,為了保證定時任務執行時間的準確性,可以修改默認配置,使其使用多線程執行定時任務
  5. 面對偶發的失敗,我們可以採用重試補償策略,不過這裡切記設置合適的最大重試次數

隨便聊聊

對於常用的開源框架,我們不僅要掌握怎麼用,還要熟悉相關的配置,最後還應該去了解其內部的使用的原理。這樣出了問題,我們也能很快定位問題,找到問題的實際原因。

關鍵字: