1. 程式人生 > 實用技巧 >延遲任務的設計思路

延遲任務的設計思路

開場白

日常的業務開發常有延遲觸發的需求,比如常見的訂單建立一段時間未付款,會自動觸發關閉;註冊使用者一段時間內未完善資料,可以觸發提醒資料更新等。這樣的需求,就是延遲排程的應用場景。

本文旨在提出一種分散式延遲排程的實現方法,為需要進行延遲排程設計的童鞋提供一種設計思路。

實現方式

實現一、QelayQueue排程實現

DelayQueue是一個高效的記憶體延時阻塞佇列,可以為任務元素增加延遲獲取的時間,從而實現在單Java程序內延遲觸發。

優點:

延遲任務可精確觸發。
缺點:

支援單程序內排程,分散式場景下的需求無法支援;
DelayQueue是記憶體佇列,程序宕機、重啟都會造成任務丟失。

實現二、週期任務 + 資料庫

將任務記錄寫入資料庫,使用固定週期從資料庫中載入延遲任務並執行,這是一種相對簡單但暴力有效的實習思路。

優點:

任務記錄寫入資料庫,可防止程序崩潰引起的任務丟失。
缺點:

週期任務掃描,臨界點任務可能到導致多延遲一個週期觸發;
如果把週期設定的很低(比如1s),則會給資料庫帶來負擔。

實現三、週期任務 + DelayQueue + 資料庫

另外一個思路:

設定一個延遲閾值,比如5m;
所有延遲任務記錄先寫入資料庫;
延遲時間在閾值的內任務直接放入DelayQueue排隊,到期則觸發;
另起一個週期任務,設定閾值執行週期,每次掃描下一個週期內任務,並載入DelayQueue排隊,到期則觸發。

通過以上操作,保證最近延遲閾值內的任務,都已經在DelayQueue中排隊等待觸發。需要注意的是,任務記錄需要有狀態,“建立”、“排隊”、“觸發成功”、“觸發失敗”,都需做狀態更新,避免任務重複執行。

實現流程可參考下圖:
在這裡插入圖片描述

優點:

延遲任務可精確觸發
任務記錄寫入資料庫,防止程序崩潰引起任務丟失;
週期載入延遲任務無需高頻率執行,減少資料庫負擔。
缺點:

週期載入延遲任務在程序內管理(未做分散式協調),多點部署的場景下,可能被多次執行,需要處理額外的爭用問題。

實現四、Quartz + DelayQueue + 資料庫實現

考慮"實現三"中在程序內週期載入延遲任務存在多次執行的問題,可引入Quartz進行任務管理,保證多點部署場景下,同一個任務只能在一臺機器上執行,從而解決多點部署的場景下任務被多次執行的問題。

實現流程可參考下圖:
在這裡插入圖片描述

優點:

延遲任務可精確觸發;
延遲任務持久化,保證不會出現任務丟失;
沒有高頻率的輪詢操作,不會給資料庫造成負擔;
一般業務量不大的系統,使用此實現完全足夠。

考慮這樣一個場景,如果一秒內要觸發的任務量很大,使用Quartz又只會在一個節點上進行延遲任務載入(Quartz不支援任務分片,只能支援失敗飄移),那延遲任務能精確觸發嗎?

實現五、分散式排程 + DelayQueue + 資料庫

分散式排程系統(xxl-job、elastic-job、tb-schedule)均支援對任務進行分片,因此使用分散式排程系統替換Quartz,週期任務使用分片策略,即可實現在多機叢集上用分片資訊載入不同的延遲任務,從而充分發揮多機並行的計算能力,儘可能的保證延遲觸發的精確性。

一些後話

1. 資料庫中延遲記錄膨脹
任務記錄在日積月累,資料庫中的延遲任務記錄會不斷膨脹。基於延遲任務特性,一般都是在一個相對較短的時間內觸發,因此可以在任務觸發成功後,將記錄遷到歷史,只在當前庫中儲存還未觸發、觸發失敗的任務,從而降低當前庫的資料量,以提升在任務表上的查詢效能。當然還可以有分庫分表等技術方案可以考慮。
2. 處理好明確失敗的任務補償
對於觸發失敗的任務,已經失去精確觸發的意義,處理時,首先可以在現場執行補償(可使用spring-retry等工具,同時需要補償回調具備冪等性),另外起一個執行緒補償任務,週期掃描延遲任務記錄表,補償觸發即可。
3. 處理好程序崩潰、重啟的任務補償
對於任務長時間處於"建立"、"排隊"等狀態的任務,應設定一定閾值(比如10m),超過延遲觸發時間點一個閾值週期的任務,理解為已經失敗,進行單獨掃描補償。