為什麼你的 Spring Task 定時任務沒有定時執行?

2020-12-13 俠夢說

前言

定時任務的使用,在開發中可謂是家常便飯了,定時發送郵件、簡訊。 避免資料庫,數據表過大,定時將數據轉儲。通知、對帳等等。

當然實現定時任務的方式也有很多,比如使用 linux 下的 contab 腳本,jdk 中自帶的 Timer 類。Spring Task 或是 Quartz 。

相信你也有過如下的疑問:

Spring Task 的 contab 的表達式 和 linux 下的 contab 有什麼區別?crontab 表達式記不住?定時任務阻塞會有什麼影響?多個定時任務的情況下是如何運行的?具有相同表達式的定時任務,他們的執行順序如何?為什麼async異步任務沒有生效?所以這篇文章,我們來介紹一下,在 Spring Task 中, 定時任務的執行原理及相關問題。演示環境為 Spring Boot 項目。

SpringBoot 定時任務的原理

相信絕大部分開發者都使用過Spring Boot 為我們提供的定時任務的 Starter 和定時任務的註解。所以我們來主要介紹一下 Spring Boot 實現定時任務的原理,和其相關註解的作用。

Spring 在 3.0版本後通過 @Scheduled 註解來完成對定時任務的支持。

在我們使用時,需要在Application 啟動類上加上 @EnableScheduling 註解,它是從Spring 3.1後開始提供的。

由於現在 Spring3 版本較低,使用得比較少了,可能並不會考慮太多細節,大多只需要關注目標實現,所以我們在配套使用兩個註解的時候,並不會出現什麼問題。

在3.0 中 ,是通過

<!-- 配置任務線性池 --> <!-- 任務執行器線程數量 --> <task:executor id="executor" pool-size="3" /> <!-- 任務調度器線程數量 --> <task:scheduler id="scheduler" pool-size="3" /> <!-- 啟用annotation方式 --> <task:annotation-driven scheduler="scheduler" executor="executor" proxy-target-class="true" />上述的 XML 配置 和 @Scheduled 配合實現定時任務的,而我們這裡的 @EnableScheduling 其實類似的和它等價,是用來發現註解了 @Scheduled 的方法,沒有這個註解光有 @Scheduled 是無法執行的,大家可以做一個簡單案例測試一下,其底層是 Spring 自己實現的一套定時任務的處理邏輯,所以使用起來比較簡單。

任務一直阻塞會怎麼樣?

介紹了兩個註解的作用後,我們來開始做實驗,簡單的寫一個定時執行的方法。

每隔 20s 輸出一句話,在輸出幾行記錄後,打上了一個斷點。

對後續的任務有什麼影響呢?

可以看到,斷點時的後續任務是阻塞著的,從圖上,我們還可以看出初始化的名為pool-1-thread-1 的線程池同樣證實了我們的想法,線程池中只有一個線程,創建方法是:

Executors.newSingleThreadScheduledExecutor();從這個例子來看,斷點時,任務會一直阻塞,當阻塞恢復後,會立馬執行阻塞的任務。線程池內部時採用 DelayQueue 延遲隊列實現的,它的特點是: 無界、延遲、阻塞的一種隊列,能按一定的順序對工作隊列中的元素進行排列。

多個定時任務的執行

通過上面的實驗,我們知道,咋看默認情況下,任務的線程池,只會有一個線程來執行任務,如果有多個定時任務,它們也應該是串行執行的。

從上圖可以看出,一旦線程執行任務1後,就會睡眠2分鐘。線程在死循環內部一直處於Running 狀態。

通過觀察日誌,根本沒有任務2的輸出,我們知道默認情況下,多個定時任務是串行執行的,類似於多輛車過單行道的橋,如果一個任務出現阻塞,其他的任務都會受到影響。

那如果線程池包含多個線程的情況下,多個定時任務並發的情況是什麼樣?

串行當然很好理解,就是上文說的汽車過橋,依次通過。再來理解並發,區別於並行,並發是指一個處理器同時處理多個任務,而並行是指多個(核)處理器同時處理多個不同的任務。並發不一定同一時間發生,而並行,指的是同一時間。

具有相同表達式的定時任務,他們的執行順序如何?

從上面的實驗同樣能知道,具有相同表達式的定時任務,還是和調度有關,如果是默認的線程池,那麼會串行執行,首先獲取到cpu時間片的先執行。在多線程情況下,具體的先後執行順序和線程池線程數和所用線程池所用隊列等等因素有關。

Spring Task和linux crontab的cron語法區別?

兩者的 cron 表達式其實很相似,需要注意的是 linux 的contab 只為我們提供了最小顆粒度為分鐘級的任務,而java中最小的粒度是從秒開始的。具體細節如下圖:

在cron語法中容易犯的錯誤

以spring 中的task為例,cron 表達式中 "/" 代表每的意思,「*/10」表示每10個單位。

在cron 語法 中很多人會犯錯誤。比如要求寫出每十分鐘定時執行的 cron 語句,可能會有以下版本的出現:

所以當我們寫完cron 表達式的時候,可以適當的調低執行間隔時間來測試,或是通過一些在線的網站來檢測你的cron腳本是否正確。

@Async異步註解原理及作用

Spring task中 和異步相關的註解有兩個 , 一個是 @EnableAsync ,另一個就是 @Async 。

首先我們單純的在方法上引入 @Async 異步註解,並且列印當前線程的名稱,實驗後發現,方法仍然是由一個線程來同步執行的。

和@schedule 類似 還是通過@Enable開頭的註解來控制執行的。我們在啟動類上加入@EnableAsync 後再觀察輸出內容。

可以發現,默認情況下,其內部是使用的名為SimpleAsyncTaskExecutor的線程池來執行任務,而且每一次任務調度,都會新建一個線程。

使用@EnableAsync註解開啟了Spring的異步功能,Spring會按照如下的方式查找相應的線程池用於執行異步方法: 查找實現了TaskExecutor接口的Bean實例。

如果上面沒有找到,則查找名稱為taskExecutor並且實現了Executor接口的Bean實例。

如果還是沒有找到,則使用SimpleAsyncTaskExecutor,該實現每次都會創建一個新的線程執行任務。

並發執行任務如何配置?

方式一,我們可以將默認的線程池替換為我們自定義的線程池。通過ScheduleConfig配置文件實現SchedulingConfigurer接口,並重寫setSchedulerfang方法。

可實現AsyncConfigurer接口複寫getAsyncExecutor獲取異步執行器,getAsyncUncaughtExceptionHandler獲取異步未捕獲異常處理器

@Configurationpublicclass ScheduleConfig implements SchedulingConfigurer { @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { taskRegistrar.setScheduler(Executors.newScheduledThreadPool(5)); }}方式二:不改變任務調度器默認使用的線程池,而是把當前任務交給一個異步線程池去執行。

@Scheduled(fixedRate = 1000*10,initialDelay = 1000*20) @Async("hyqThreadPoolTaskExecutor") public void test(){ System.out.println(Thread.currentThread().getName()+"--->xxxxx--->"+Thread.currentThread().getId()); } //自定義線程池 @Bean(name = "hyqThreadPoolTaskExecutor") public TaskExecutor getMyThreadPoolTaskExecutor() { ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); taskExecutor.setCorePoolSize(20); taskExecutor.setMaxPoolSize(200); taskExecutor.setQueueCapacity(25); taskExecutor.setKeepAliveSeconds(200); taskExecutor.setThreadNamePrefix("hyq-threadPool-"); taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); taskExecutor.setWaitForTasksToCompleteOnShutdown(true); taskExecutor.setAwaitTerminationSeconds(60); taskExecutor.initialize(); return taskExecutor; }其他問題

如果是定時任務沒有生效,需要檢查 @EnableScheduling 註解是否加上。 如果是異步沒有生效,需要檢查 @EnableAsync 註解是否加上,並且定義線程池,否則仍然是串行執行的。

總結

文章介紹了SpringBoot 定時任務的原理, 3.0版本前後的區別,通過單線程任務阻塞實驗,探究了延遲隊列及串行、並行、並發的概念。對比了linux下的 contab 和spring的cron表達式區別以及常犯的錯誤。最後通過實驗異步註解,兩種方式配置線程池,讓任務高效運作,希望本文能讓你有所收穫。

創建了一個java方面的互助群,和其他傳統的學習群不同。

本群主要致力於解決項目中的疑難問題,在遇到項目難以解決的

在本群,你可以

1)闡述你在開發過程中遇到的問題,群友集思廣益,高效解答。

2)分享自己學習的一些心得,讓後來學習者少踩坑。

3)資源共享,無論是好的學習視頻還是文檔都可以在群內共享。

別人有可能可以給你提供一些思路和看法

同樣,如果你也樂於幫助別人,那解決別人遇到的問題,也同樣對你是一種鍛鍊。

想邀你加入這個有溫度的社群

分享經驗和心得

集思廣益,高效解答問題

幫助他人,鍛鍊自己

相關焦點

  • 實戰Spring Boot 2.0系列:單機定時任務的幾種實現
    常見的就是 金融服務系統推送回調,一般支付系統訂單在沒有收到成功的回調返回內容時會 持續性的回調,這種回調一般都是 定時任務 來完成。使用這種方式可以讓你的程序按照某一個 頻度執行,但不能在 指定時間 運行。現在一般用的較少。
  • Spring Boot實現定時任務新解,你是否能get到?
    在日常的開發過程中經常使用到定時任務,在springMVC的開發中,經常和quartz框架進行集成使用,但在springboot中沒有這麼做,而是使用了java的線程池來實現定時任務。一、概述在springboot中使用定時任務非常簡單,只需要簡單的幾步即可完成。
  • Spring 定時任務玩出花!
    當有一個新的定時任務需要執行時,創建一個 SchedulingRunnable 線程,然後連同 Cron 表達式一起扔到 ThreadPoolTaskScheduler 池子裡去執行就行了。3. 配置分析幾個配置類我們逐一來分析。
  • 學會這 10 種定時任務,我有點飄了
    前言在不用的業務場景下要用不同的定時任務,其實我們的選擇還是挺多的。本文總結了 10 種非常實用的定時任務,總有一種是適合你的。一. linux 自帶的定時任務crontab不知道你有沒有遇到過這種場景:有時需要臨時統計線上的數據,然後導出到 excel 表格中。
  • 我有 10 種方法搞定定時任務,10種!
    一. linux自帶的定時任務crontab不知道你有沒有遇到過這種場景:有時需要臨時統計線上的數據,然後導出到excel表格中。這種需求有時較為複雜,光靠寫sql語句是無法滿足需求的,這就需要寫java代碼了。然後將該程序打成一個jar包,在線上環境執行,最後將生成的excel文件下載到本地。
  • 初探SpringBoot整合Quartz定時任務
    定時任務有很多實現的方式,包括timer,timertask,scheduledexecutorservice,以及第三方框架Quartz。本篇文章主要介紹SpringBoot整合Quartz實現動態定時任務。1、Quartz是功能強大的開源作業調度庫,可以創建簡單或複雜的計劃,可以運行十個,百個,甚至幾萬個Jobs這樣複雜的日程序表。
  • Spring-Task源碼解析
    >任務的定時調度/執行,對應xml配置的task:scheduler和task:scheduled-tasks標籤。/></task:scheduled-tasks>定義了一個定時任務,每隔5秒執行Task的print方法,Task:publicclassTask{publicvoid
  • Python定時任務(上)
    其一:定時執行任務。例如每天早上 8 點定時推送早報。其二:每隔一個時間段就執行任務。比如:每隔一個小時提醒自己起來走動走動,避免長時間坐著。今天,我跟大家分享下 Python 定時任務的實現方法。第一種辦法是最簡單又最暴力。
  • SpringBoot定時任務:schedule、quartz
    Scheduled只適合處理簡單的計劃任務,不能處理分布式計劃任務。優勢:是spring框架提供的計劃任務,開發簡單,執行效率比較高。且在計劃任務數量太多的時候,可能出現阻塞,崩潰,延遲啟動等問題。Scheduled定時任務是spring3.0版本之後自帶的一個定時任務。
  • python定時任務管理
    '''def timedTask():''' 第一個參數: 延遲多長時間執行任務(單位: 秒) 第二個參數: 要執行的任務, 即函數 第三個參數: 調用函數的參數(tuple) '''Timer(10, task, ()).start()# 定時任務
  • Java定時任務的五種創建方式,你都會麼?
    ,使用Quartz表達式的定時任務如下一、job 定時任務的五種創建方式/**  * TODO  使用線程創建 job 定時任務  * @author 王松  */public class JobThread {    public static class Demo01 {
  • 基於Spring整合Quartz集群的定時任務應用
    1 基於Spring整合Quartz集群的定時任務應用概述雖然單個Quartz實例能給予你很好的Job調度能力,但它不能滿足典型的企業需求,如可伸縮性、高可靠性滿足。假如你需要故障轉移的能力並能運行日益增多的 Job,Quartz集群勢必成為你應用的一部分了。使用 Quartz 的集群能力可以更好的支持你的業務需求,並且即使是其中一臺機器在最糟的時間崩潰了也能確保所有的 Job 得到執行。Quartz 中集群如何工作一個 Quartz 集群中的每個節點是一個獨立的 Quartz 應用,它又管理著其他的節點。
  • 定時任務實現原理詳解
    一、摘要在很多業務的系統中,我們常常需要定時的執行一些任務,例如定時發簡訊、定時變更數據、定時發起促銷活動等等。同時,細心的發現,這個方案還有以下幾個缺點:串行阻塞:調度線程只有一個,長任務會阻塞短任務的執行,例如,A任務跑了一分鐘,B任務至少需要等1分鐘才能跑容錯能力差:沒有異常處理能力,一旦一個任務執行故障,後續任務都無法執行3.2、ScheduledThreadPoolExecutor
  • PHP在Linux下執行定時任務的實現思路詳解
    PHP本身是沒有定時功能的,PHP也不能多線程。PHP的定時任務功能必須通過和其他工具結合才能實現,例如WordPress內置了wp-cron的功能,很厲害。/cron-run'); // 這裡就是通過刪除cron-run來告訴程序,這個定時任務已經在執行過程中,不能再執行一個新的同樣的任務$loop = $interval; } while(true);通過執行上面這段php代碼,即可實現定時任務,直到你刪除cron-switch
  • PHP實現執行定時任務的幾種思路詳解
    『/cron-run『); $loop = $interval;} while(true);通過執行上面這段php代碼,即可實現定時任務,直到你刪除cron-switch文件,這個任務才會停止。但是有一個問題,也就是如果用戶直接訪問這個php,實際上沒有任何作用,頁面也會停在這個地方,一直處於加載狀態,有沒有一種辦法可以消除這種影響呢?
  • php Swoole實現毫秒級定時任務
    項目開發中,如果有定時任務的業務要求,我們會使用linux的crontab來解決,但是它的最小粒度是分鐘級別,如果要求粒度是秒級別的,甚至毫秒級別的,crontab就無法滿足,值得慶幸的是swoole提供的強大的毫秒定時器。應用場景舉例我們可能會遇到這樣的場景:以上的三個場景我們都可以歸納為定時任務的範疇。
  • OpenResty使用ngx.time.every完成定時任務
    OpenResty方法說明OpenResty中的ngx.timer.every可創建後臺定時任務,配合init_worker_by_lua*階段可完成定時任務初始化。delay: 指定延遲時間執行,單位秒。不可以為0callback: 回調函數,在入參中加入premature會讓Nginx內核自動調用。user_atg1...: 調用時接收的參數。
  • 【安卓按鍵精靈】定時執行指定任務
    說到「定時」有這麼幾種常見的形式:(1)每天固定時間執行,就像手機鬧鐘一樣,到時間就會執行操作。
  • Asp定時執行操作、Asp定時讀取資料庫(網頁定時操作詳解)
    >你可以在同一個頁面重複刷新,以達到定時操作的效果。  setTimeout方法是定時程序,也就是在什麼時間以後幹什麼。幹完了就拉倒。  setInterval方法則是表示間隔一定時間反覆執行某操作。  如果用setTimeout實現setInerval的功能,就需要在執行的程序中再定時調用自己才行。
  • Python 實現定時任務的八種方案!
    在日常工作中,我們常常會用到需要周期性執行的任務,一種方式是採用 Linux 系統自帶的 crond 結合命令行實現。另外一種方式是直接使用Python。接下來整理的是常見的Python定時任務的實現方式。