空間寵物業務需要實作一個定時消息觸發元件,如在特定時刻給使用者推送收集糖果通知、biubiu球功能定時回收使用者丢棄的球等。可見,消息隻有在特定時間到達才能被處理。同時,消息的産生是無序的,即後産生的消息被處理的時間可能早于先産生的消息。
一些著名的消息隊列元件,如ActiveMQ ,本身支援消息延遲投遞,為何本文選擇Redis呢?一方面是引入新組建有學習、運維、接入成本,而組内已積累一定Redis開發運維經驗;另一方面則是基于Redis實作這樣一個元件難度也不大。是以決定采用Redis。
鍵空間通知可以在消息到達時插入一個key,并給key設定過期時間,鍵過期後會通過特定頻道釋出鍵過期通知,訂閱方可收到通知并處理事件。但問題在于:
key過期并不保證立即删除,Redis隻會每次執行server.c:databasesCron時随機删除若幹key,大量key同時過期無法保證時效;
Pub/Sub機制不保證通知送達,若client掉線則通知丢失;
若多個client同時訂閱,則都會收到通知,導緻重複處理。
ZSET可在消息插入時根據score排序,進而使最早的消息排在最前面。但ZSET沒有提供POP方法,取得第一個元素和删除需要執行兩個指令。為保證原子性,可以采用事務,如:
或者使用pipelining,如:
但問題在于,雖然可順序取出消息,但無法隻在時間到達後取出消息。是以需要client端實作邏輯等待時間到達再推送。同時,消息産生是無序的,如果取得了一個10分鐘後處理的消息,在此期間又産生了一個需要在5分鐘後處理的消息,邏輯将變得複雜。
由于使用原生Redis無法滿足需求,我們決定擴充Redis指令。
LUA腳本是利用3.X版官方特性實作指令擴充的途徑。以下腳本将讀出首元素,并與目前時間戳(以參數傳入)比較,如果消息處理時間到達則删除消息并傳回;所有操作将是原子的。目前我們線上服務使用該方案。
LUA腳本:
client生成指令:
缺點是:
使用lua腳本有額外學習成本
實作在用戶端,無法很好的複用
使用lua後要做好運維工作,配置腳本逾時,注意腳本緩存記憶體占用
改源碼,加一個指令。我們較早上線的一個服務使用了該方案。
缺點是:後續官方更新都需要改代碼。
使用[Redis 4.0子產品實作。此處是GitHub傳送門。
相比前兩種方法,此方法邏輯收歸在服務端,且不需要修改Redis源碼便于更新。但需要注意資源釋放、複制機制等細節,謹防踩坑。
1 . 相容性:要求所有從機、或加載AOF/RDB的執行個體均實作了新的指令,即均為修改版Redis或均加載了擴充子產品。
2 . 指令寫入AOF和從機的時機:
對于3.2.X使用LUA法,預設複制腳本本身,但可以使Redis僅複 制導緻變更的指令而非整個指令,參考腳本中有關”Replicating commands instead of scripts”和”Selective replication of commands”的内容。
對于3.2.X版本修改源碼法,在server.c:call中,僅當有變更設定dirty變量值大于0時,才會觸發指令傳播,是以如果指令沒有成功pop元素将不會産生指令傳播。
對于4.0 modules,我們的實作中使用了低級API,則需要實作中根據需要調用RedisModule_ReplicateVerbatim複制指令。
3 . 消息處理失敗處理:ZSET中消息被pop後才被client取得處理,若client處理失敗則需要client在保證幂等的前提下自行重試。