說說 Spring 定時任務如何大規模企業級運用

語言: CN / TW / HK

作者: 姚輝(千習)

Spring 定時任務簡介

定時任務是業務應用開發中非常普遍存在的場景(如:每分鐘掃描超時支付的訂單,每小時清理一次資料庫歷史資料,每天統計前一天的資料並生成報表等等),解決方案很多,Spring 框架提供了一種通過註解來配置定時任務的解決方案,接入非常的簡單,僅需如下兩步:

  1. 在啟動類上添加註解@EnableScheduling

@SpringBootApplication @EnableScheduling // 新增定時任務啟動註解 public class SpringSchedulerApplication { public static void main(String[] args) { SpringApplication.run(SpringSchedulerApplication.class, args); } }

  1. 開發定時任務 Bean 並配置相應的定時註解@Scheduled

``` @Component public class SpringScheduledProcessor {

/ * 通過Cron表示式指定頻率或指定時間 / @Scheduled(cron = "0/5 * * * ?") public void doSomethingByCron() { System.out.println("do something"); }

/* * 固定執行間隔時間 / @Scheduled(fixedDelay = 2000) public void doSomethingByFixedDelay() { System.out.println("do something"); }

/**
 * 固定執行觸發頻率
 */
@Scheduled(fixedRate = 2000)
public void doSomethingByFixedRate() {
    System.out.println("do something");
}

} ```

Spring 定時任務原理

執行原理

Spring 定時任務核心邏輯主要在 spring-context 中的 scheduling 包中,其主要結構包括:

  • 定時任務解析:通過 ScheduledTasksBeanDefinitionParser 對 XML 定義任務配置解析;也可通過 ScheduledAnnotationBeanPostProcessor對@Scheduled 註解進行任務解析(常見模式)。 

  • 定時任務註冊登記:上述解析獲得的 Task 任務配置會被註冊登記至 ScheduledTaskRegistrar 中以備執行使用。 

  • 任務定時執行:完成所有任務註冊登記後,會通過 TaskScheduler 正式地定時執行相關任務,底層通過 JDK 的 ScheduledExecutorService 執行任務。 

1.jpeg

業務邏輯會將被包裝在 ScheduledMethodRunnable 類中,其中包含了待執行的目標業務物件 Bean 和業務方法,該 Runnable 物件在執行時會被提交至 ScheduledExecutorService 排程執行緒池完成任務的定時執行。

2.jpeg

從上圖可以看到真正要執行的業務邏輯 ScheduledMethodRunnable 會被 ReschedulingRunnable、DelegatingErrorHandlingRunnable 做了代理擴充套件,這兩層代理擴充套件具有如下意義:

  • DelegatingErrorHandlingRunnable:為業務方法執行異常進行包裝處理,提供了自定義異常處理機制、解決 JDK 原生定時任務執行異常後任務失效問題。 

  • ReschedulingRunnable:提供了擴充套件的定時模式支援,可支援基於 Trigger 介面自定義實現獲取下次觸發時間定時排程,預設提供的 Cron 定時通過此方式進行擴充套件實現。

定時模式

Spring 定時任務 Task 類的模式主要可分為兩類:IntervalTask 和 TriggerTask。前者表示固定頻率間隔執行,後者則採用 Trigger 觸發器模式實現定時排程,Cron 表示式配置為該模式實現。

  • FixedDelay:按固定延遲頻率執行,任務下一次觸發時間=上一次執行結束時間+Delay 延遲時間。 

3.jpeg

  • FixedRate:按固定頻率觸發執行,任務下一次觸發時間=上一次觸發時間+Delay 延遲時間。如果上一次執行方法不結束會阻塞下一次任務執行。 

4.jpeg

  • Cron 表示式:按 Cron 表示式計算下一次觸發時間,任務下一次觸發時間=cron(上一次執行結束時間)。

進階擴充套件

  • 執行緒池執行

預設配置下底層執行的執行緒池為單執行緒,單執行緒的執行模型在任務量較多且觸發頻率較高的情況下,一旦某個任務發生阻塞會導致所有後續定時任務執行阻斷,這對業務執行帶來嚴重隱患。常見可採用如下方式:

  • 配置定時執行執行緒池:常見基於配置 Spring Boot 配置(spring.task.scheduling.pool.size=執行緒數),執行緒數大小取決於任務數及排程頻率合理配置。 

  • 配置非同步任務:在 spring context 中的 scheduling 模組下提供了@EnableAsync 和@Async,可用於開啟任務非同步執行,實現定時排程執行緒池非阻塞執行。該模式下存在一些不足之處:異常處理需要走非同步呼叫的 AsyncUncaughtExceptionHandler 異常處理介面實現,同步/非同步定時任務異常處理機制不統一,另外非同步模式增加了業務應用的執行緒開銷。 

@Scheduled(fixedDelay = 2000) @Async public void test() { System.out.println(DateUtil.now()+ " test."); }

  • 異常統一處理

定時任務執行可設定統一異常處理,基於 ErrorHandler 介面開發對應異常處理實現類。對應的異常實現處理類需要注入到核心的 ThreadPoolTaskScheduler 中,使用者可以通過自定義 TaskSchedulerCustomizer 方式來實現 ErrorHandler 自定義異常處理 Bean 注入至 ThreadPoolTaskScheduler 中。

``` @Component public class DemoTaskSchedulerCustomizer implements TaskSchedulerCustomizer { @Override public void customize(ThreadPoolTaskScheduler taskScheduler) { taskScheduler.setErrorHandler(new DemoErrorHandler()); }

private class DemoErrorHandler implements ErrorHandler {
    @Override
    public void handleError(Throwable throwable) {
        System.out.println("異常統一處理.");
    }
}

} ```

原生 Spring 定時任務在企業中遇到的問題

任務重複執行

Spring 定時任務,只要有註解就會執行,在分散式場景下,所有機器程式碼一致,會導致同一個任務在多臺機器上重複執行。一般的解決方案是搶鎖觸發,分散式鎖實現形式可採用 DB、ZK、Redis 等方式。

5.jpeg

示例程式碼如下:

``` @Component @EnableScheduling public class MyTask { / * 每分鐘的第30秒跑一次 / @Scheduled(cron = "30 * * * ?") public void task1() throws Exception { String lockName = "task1"; if (tryLock(lockName)) { System.out.println("hello cron"); releaseLock(lockName); } else { return; } } private boolean tryLock(String lockName) { //TODO return true; }

private void releaseLock(String lockName) {
    //TODO
}

} ```

如上圖所示,當任務觸發時 3 個 server 會對任務搶鎖,僅獲得任務鎖的 server 才能執行對應任務業務邏輯。當前的這個設計,仔細一點的同學可以發現,其實還是有可能導致任務重複執行的。比如任務執行的非常快,A 這臺機器搶到鎖,執行完任務後很快就釋放鎖了。B 這臺機器後搶鎖,還是會搶到鎖,再執行一遍任務。

無管控無運維

原生 Spring 定時任務沒有控制檯,無法動態的新增和修改定時任務,如果要修改定時任務的配置(比如每分鐘跑一次改成每小時跑一次),必須修改程式碼重新發布應用。同時原生Spring定時任務也沒有運維操作,不支援執行一次任務,任務失敗了也不支援重跑任務。

如果要自研的視覺化控制檯來實現整套任務視覺化管控體系,需要一定的前後端研發成本和服務部署成本投入。對於需要自建的使用者而言,可參考以下需求功能進行自有平臺建設:

  • 任務的視覺化動態配置
  • 任務執行執行詳細資訊的視覺化檢視
  • 任務執行日誌、執行呼叫鏈、排程觸發的視覺化查詢分析
  • 業務應用間任務資訊配置許可權隔離

無業務失敗通知能力

對於完整企業級定時任務運用方案中,報警通知能力必不可少,任務跑失敗了需要及時通知到使用者,否則可能產生故障。

原生 Spring 定時任務不支援報警通知能力,如果要自研,可以參考上一章節中《異常統一處理》對任務失敗的資訊進行收集,構建相應的異常處理機制(包括對接各類報警平臺進行異常訊息通知處理,定義異常等級和類別進行不同的通知策略),然後進行定時任務報警通知。

6.jpeg

無線上排查分析能力

定時任務在執行過程中會存在各種各樣的問題,比如:執行失敗、執行耗時、執行卡住等,這些都需要在後期實際運維去定位快速分析。在對應分析過程中沒有高效線上排查能力的話將遇到很多棘手的問題:

  • 叢集中任務對應時間點是跑在哪個機器上無從可知
  • 需要在大量的業務應用日誌中去檢索對應時點的定時任務執行日誌,需要自行對接日誌服務改善
  • 如果任務涉及多個跨服務呼叫,無法定位執行異常點或執行耗時點,需要自建全鏈路追蹤來支援

阿里雲 Spring 定時任務企業級解決方案

接下來主要講下如何利用公有云上任務排程 SchedulerX 輕鬆接入基於 Spring 開發的定時任務。前面聊了基於 Spring 原生功能在使用過程中面臨的問題及需要自行處理解決的相關方案,可以看到僅針對企業級最基礎的運用場景下就需要花費較多的改造投入及相關服務後續運維投入。通過接入 SchedulerX 任務排程平臺,原本 Spring 定時任務使用者可無縫且 0 改造獲得企業級運用所需能力,同時降低了自研部署運維定時服務相關元件的技術成本。

如何接入

對於 SchedulerX 新使用者而言接入僅需三步(參考附件接入手冊):

  • 依賴 SchedulerX 的 Spring Boot 版 SDK 完成排程平臺接入(版本>=1.7.2,老使用者僅升級 SDK 版本即可) 

  • 配置檔案新增配置項,配置開啟後 Spring 定時排程器將不執行相關任務(未配置情況下,不會主動接管原 Spring 定時任務執行,在配置開啟前不會影響原本定時任務業務執行)

```

配置表示由SchedulerX接管Spring定時任務執行

spring.schedulerx2.task.scheduling.scheduler=schedulerx ```

  • 控制檯上在對應應用分組下建立任務配置定時觸發。也可以選擇開啟自動同步任務配置方式(可選)

```

自動同步Spring定時任務至排程平臺,無需單獨手動建立(預設不開啟)

spring.schedulerx2.task.scheduling.sync=true ```

接入優勢

  • 白屏管控和運維

提供白屏控制檯可以動態新增、修改、啟用、禁用任務,支援執行一次、原地重跑、重刷資料、停止任務、標記成功等運維操作。

7.png

8.png

9.png

  • 視覺化線上排查問題

支援執行記錄檢視、執行業務日誌查詢、執行全鏈路追蹤。

10.png

11.png

  • 豐富的報警通知

SchedulerX 提供豐富的報警通知能力,支援簡訊、電話、郵件、webhook 報警,支援報警聯絡人組和報警歷史,可白屏動態配置。

12.png

13.png

  • 其他優勢

  • 無改造成本的平臺接入方案。

  • 無需額外獨立運維排程服務平臺或其他第三方元件服務。
  • 任務執行在叢集環境中具備穩定高可靠支援,規避了原生框架存在的重複執行問題,具備故障自動轉移能力。
  • 在企業內多個團隊可共享一套平臺使用,通過名稱空間和應用分組實現各團隊任務配置資料隔離及環境隔離。

總結

本文主要從 Spring 定時任務的執行機制進行剖析闡述,並對如何擴充套件框架原生能力以滿足企業級生產環境執行定時任務所需各種場景提出了相應的建議,使用者可作參考構建自己內部定時任務方案。同時就阿里雲上提供的任務排程服務如何接入 Spring 定時任務的執行進行講解,並簡單展示了接入後所帶來的企業級能力。最後歡迎有定時任務業務需求使用者可先通過基礎免費額度體驗感受雲上服務帶來便捷。

附錄

[1] spring scheduling 使用手冊:

http://docs.spring.io/spring-framework/docs/5.2.x/spring-framework-reference/integration.html#scheduling-enable-annotation-support

[2] spring任務接入手冊:

http://help.aliyun.com/document_detail/450857.html