最近業務上有個需求,背景以下:有一個養殖類遊戲,經過給養的寵物餵食來升級,一次餵食後,寵物須要花4個小時吃完。如今有個新需求,可使用道具卡來豐富玩法。道具卡有兩種,一種是加速卡,一種是自動餵食卡。加速卡會使吃食的時間縮短兩個小時,自動餵食卡能夠在寵物吃完當前餵食的狗糧後系統幫助其自動餵食一次。redis
業務需求裏的自動餵食就是一種典型的延時任務。延時任務是指須要在指定的將來的某個時間點自動觸發。與之相似的場景還有:數據庫
對於延時任務,常見的方案就是掃表。掃表就是用一個後臺進程,每隔一段時間掃描數據庫的整張數據表,判斷每一個任務是否達到觸發的條件。若是達到條件就執行相應的業務。掃描全表對數據庫壓力較大,因此通常選擇掃從庫。掃表的最大優點是實現起來比較簡單,並且數據自己存在DB裏,所以也不用擔憂任務數據會丟失,失敗的任務能夠下次掃描時再重入。可是掃表存在如下問題:網絡
掃表最大的問題就是會有延遲,不能再指定的時間裏觸發,對於時效性高的場景,這種方案是不能知足需求的。數據結構
目前,有些MQ消息隊列能夠支持延時消息,如kafka。延時消息就是消息發送後,能夠指定在多少時間以後纔會發送到消費者那裏。這個方案,開發成本也很小,不過須要使用的中間件能支持延時消息。並且該方案也存在一個瓶頸就是若是,延時任務須要從新更新時間就作不到了,由於消息已經發出去了,收不回了。架構
用環形隊列作成時間片,環形隊列的每一個格子裏維護一個鏈表。每一個時刻有一個當前指針指向環形隊列某個格子,定時器每超時一次,就把當前指針指向下環形隊列的下一個格子。而後處理這個格子保存的鏈表裏的任務。若是隻是這樣維護,若是要作到秒級的粒度,時間長度最長一天,那麼這個環形隊列就會很是大。所以,有人又有人改進了一下,當存在任務進入隊列時,就用時間長度除以環形隊列的長度,記爲圈數。這樣每次遍歷到該元素時,將圈數減一,若是減一後爲0就執行改任務,否者不執行。併發
kafka的延時消息的內部實現就是採用時間片輪詢的方式來實現的。異步
對於時間跨度很是大的場景,若是使用這種方法會致使鏈表上的元素很是多,遍歷鏈表的開銷也不小,甚至在一個時間片內遍歷不完。所以,又有了進一步的改進,將時間片分爲不一樣粒度的。好比,粒度爲小時的時間輪,粒度爲分鐘的時間輪,粒度爲秒鐘的時間輪。小時裏的時間輪達到觸發的條件後會放到分鐘的時間輪裏,分鐘的時間輪到達觸發的條件後會放到秒的時間輪裏。(圖片來自網絡,侵刪)分佈式
該方案時間片存放在內存,所以輪詢起來效率很是高,也能夠根據不一樣的粒度調整時間片,所以也很是靈活。可是該方案須要本身實現持久化與高可用,以及對儲存的管理,若是沒有現成的輪子開發耗時會比較長。高併發
Redis實現延時任務,是經過其數據結構ZSET來實現的。ZSET會儲存一個score和一個value,能夠將value按照score進行排序,而SET是無序的。ui
延時任務的實現分爲如下幾步來實現:
(1) 將任務的執行時間做爲score,要執行的任務數據做爲value,存放在zset中; (2) 用一個進程定時查詢zset的score分數最小的元素,能夠用ZRANGEBYSCORE key -inf +inf limit 0 1 withscores命令來實現; (3) 若是最小的分數小於等於當前時間戳,就將該任務取出來執行,不然休眠一段時間後再查詢
redis的ZSET是經過跳躍表來實現的,複雜度爲O(logN),N是存放在ZSET中元素的個數。用redis來實現能夠依賴於redis自身的持久化來實現持久化,redis的集羣來支持高併發和高可用。所以開發成本很小,能夠作到很實時。
掃表的方法延時過高不能知足實時的需求,團隊目前使用的消息隊列還不支持延時消息隊列,時間輪的方法開發起來很耗時,所以最終選擇了Redis來實現。
前面介紹了Redis實現延時任務的原理,爲了實現更高的併發還須要在原理的基礎上進行設計。接下來將詳細闡述具體的實現。架構設計圖以下:
說明:
在延時任務的基礎上,本次業務還有一個需求,就是延時任務若是尚未到達執行時間,那麼該延時任務的時間是能夠被更改的。爲了實現這個需求,咱們另外給每一個用戶維護一個ZSET,這個ZSET中存放該用戶全部的延時任務。爲了便於描述,咱們將這個ZSET稱爲ZSET-USER。若是用戶須要修改其延時任務,若是沒有辦法從總體的延時任務的ZSET中找到這個任務,而是即便能找到,也只能遍歷這個ZSET,顯然這種方法太慢,太耗資源。咱們採起的方法是從ZSET-USER中取出這個用戶的延時任務,而後修改score,最後從新ZADD到延時任務ZSET和ZSET-USER中,ZADD會覆蓋原來的任務,而score則發生了更新。這樣看來,這個需求還只能經過Redis來實現。
本篇文章,藉着業務需求的背景首先探討了延時任務的業界實現方案,而後詳細闡述了經過redis來實現延時任務方法,並分析了高併發,高可用的設計思路。