延時任務排程系統——技術選型
經常會有這樣的需求,包含大量的延時執行任務
1,如一個代辦事項app,代辦實現可以設定觸發時間,像鬧鐘一樣。
2,如美團的訂單,下單後10分鐘不支付,會自動取消。
3,淘寶使用者7天不確認收貨,自動確認收貨。
諸如以上需求,需要的就是一個延時任務執行系統。實現的方案有很多,各有優劣:
一,為每個任務建立一個倒計時執行緒
優點:實現簡單,及時性高
缺點:嚴重浪費伺服器CPU資源,如果資料量巨大,要建立多少執行緒啊;
執行緒和資料存在於記憶體中,宕機或redeploy後無法恢復;
二,定時任務掃描
利用定時任務輪詢資料庫,掃描出來到時間的資料。
優點:實現簡單
缺點:掃描頻率高的話,會浪費大量的伺服器資源;
掃描頻率低的話,及時性無法保證;
掃描頻率中等的話,既浪費伺服器資源,又無法保證及時性。
三,改良的定時任務掃描
將過期時間分為多個階段,如下:
1)大於1天 1天掃描一次
2)小於1天,大於1小時 1小時掃描一次
3)小於1小時,大於1分鐘 1分鐘掃描一次
4)小於1分鐘 1秒鐘掃描一次
如上,分為4個階段(可根據具體情況再次細分),開啟4個定時任務分別進行掃描,掃描頻率由低到高,將記錄id儲存到4個redis的list中。掃描時,1)中符合條件的要被move到2)的list中,2)中符合條件的要被move到3)的list中,以此類推,4)中掃描到後立即開啟Timer或Schedule進行倒計時執行。
優點:可大大降低每次掃描所需處理的資料;
缺點:依然是定時輪詢,效率不高,伺服器資源也有浪費;
實現也複雜;
四,Rabbitmq
向rabbitmq釋出訊息時可以指定其ttl(time-to-live),訊息過期時會進入dead-letter-exchange
優點:高效,可以利用rabbitMq的分散式特性進行叢集擴充套件,且支援持久化
缺點:已釋出的訊息無法撤銷或修改;
一個訊息比同一佇列的其它訊息提前過期,也無法優先進入死信佇列;
五,Redis的SortedSet
Redis的sortedSet是個有序佇列,將過期時間作為其score,放入sortedSet中時,redis會自動按score排序。開啟一個定時任務,不斷判斷第一個元素是否到期,如果是,則從佇列中移除,並執行相關業務處理。
優點:實現簡單;
任務可撤銷,可修改;
redis可叢集;
缺點:需要有定時任務高頻率的訪問redis,判斷是否過期,造成CPU空耗;
六,Redis的expired_time
向redis中新增資料時指定其過期時間,並監聽其過期事件
jedis.psubscribe(new JedisPubSub(), "__key*@0__:expired");
其中0是redis的DB_INDEX
優點:高效;
實現簡單;
缺點:暫無發現
七,DelayQueue
jdk中的DelayQueue就是為了這種場景而設計的,完美契合需求,將資料放入佇列時需要給出其過期時間,DelayQueue會自動將其排序,呼叫take方法,如果沒有到期的資料,執行緒會阻塞住,直到有資料到期。
但這種方式也有缺點:
1)資料存在記憶體中,需要持久化(可以自己實現),否則宕機或redeploy會丟失資料;
2)不具備分散式能力,無法叢集;
八,時間輪
1)時間輪是一種環裝的資料結構,分成多個格子;
2)每個格子代表一段時間,時間越短,精度越高;
3)每個格子裡都有一個任務list;
4)指標隨著時間一格一格的轉動,並執行相應格子中的任務;
以上圖為例,假設一個格子是1秒,則整個時間輪能表示的時間段為8s, 如果當前指標指向2,此時需要排程一個3s後執行的任務,需要放到第5個格子(2+3)中,指標再轉3次就可以執行了。
缺點:1)格子數量有限,所能代表的時間有限,太多不好維護。
2)如果時間跨度大,每次檢查的量很大,會做很多無效的檢查。
3)實現複雜,尤其是下面的分層時間輪。
為解決問題1,可將任務的輪次放入到格子中,如一個任務是10s後執行,就是第二個輪次的第2s執行,放入當前指標的後面第2個格子,且標記其輪次為2,指標也要校驗輪次。
為解決問題2,可以做分層時間輪,如圖:
分層時間輪中的每個輪都有自己的格子數和時間間隔,當最底層的時間輪轉一圈時,高一層的時間輪就轉一個格子。高層的格子中的任務進入尾聲時,需要降級到底層時間輪。和方案三類似。
九,三方中介軟體
這種型別的中介軟體很少,有如下:
以上9種方案,五六七實現簡單(其中7可自己實現下持久化),推薦
說明:
1,本文只提供大致思路,不提供具體實現
2,本文部分思路來源於網路,侵刪