消息隊列(MQ)
相信大家對MQ
這個詞都不會陌生,不管用過還是沒用過的,大多會對他有一定的了解,
那么消息隊列有什么好處呢
- 解耦(接觸服務之間的耦合度關系)
- 削峰(例如我某個促銷活動在某個時間點有非常大的流量涌入,這個時候用Mq做緩存是最好的方式了)
- 異步化(例如有些服務是我不需要在同步鏈中進行調用的,那么可以用mq來做一個異步消費)
傳統MQ的缺點
MQ基本上和緩存一樣是居家必備之良藥。然而消息隊列雖然重要,但是同時其實是蠻重的一個組件。例如我們在用rabbitMq的話,我們需要為它搭建一個服務端,如果考慮到可用性,那么我們需要為服務端建立一個集群,同時,我們如果線上問題可能還需要在Mq中做查找,那么這些工作就可能加大我們整體的工作量。
利用redis來實現MQ
所以就想能不能先簡單的通過Redis來實現消息隊列呢?不考慮PubSub、分布式、持久化、事務等復雜的情況。就像JDK的各種Queue一樣。答案當然是可以的,因為Redis提供的list數據結構就非常適合做消息隊列。大家可能會發現,網上有很多redis的消息隊列,但是目前為止,我沒有發現一個消息隊列是具有ack機制的。
這里我們會講述怎么利用list的api中的lpush/brpoplpush來實現一個具有ack機制的消息隊列
實現思路
初步實現
實現ack的話,(暫時先不考慮集群版,只是單機版本)
- 我可以用lpush做生產者,每次有消息需要生產的時候,就發送一個message到pending隊列中。
- brpoplpush做消費者,每次取到消息的時候進行業務消費。在消費的同時吧消息放到另一個doing的隊列中
- 每次消費者完成任務,從doing隊列中刪除任務msg,用來告知這個消息被成功消費掉了
- 然后開一個線程去定時輪詢查doing中,如果一定時間(架設我們的message實現了我們的協議,message中帶有任務開始的時間戳),這個任務還沒被消費成功,那么就把這個doing隊列的那個就重新塞到pending的隊列里
發現問題
但是這時候可能會出現這樣的問題,我輪詢doing的隊列在取任務的時候可能因為我消費者的任務因為某些原因做的慢了些,那么這時候就會被重新塞會pending隊列里,但是過兩秒我的doing確實消費完了。
那么怎么解決這個問題呢?
解決方式其實很簡單,就是上面的進行步驟3的時候,如果從doing隊列進行刪除的時候,如果返回值表示刪除失敗的話,那么說明我們的任務被系統認為過期了,他被賽入pending中了,那么我們只需要在這個時候去pending中重新刪除這個message消息即可
延伸問題
ok,那么大家覺得這時候已經完工了嗎?其實并沒有。。。為什么呢?
因為會出現如下這樣一種比較極端的情況:
就是任務完成之后去doing隊列中刪除message失敗,然后去pending中刪除也失敗,因為有可能在任務掃描的時候,吧任務剛放入pending隊列中,沒等doing完成呢,pending中重新放入的任務就被消費了。那么這時候依然是消息出現重復
這種情況下的最佳解決方案是什么呢?就是消費端做好冪等性處理(其實像阿里的RocketMq)也會出現消息重復的情況(雖然極低概率),但是在Mq中,似乎設計一個精確只發一次的模型,是一件比較難的事情。
深層延伸的方案
上面的消息重復其實還是有優化的余地,具體的實現思路如下:
- 優化掃描的模型,吧掃描doing過期任務變成一個延遲掃描(如用delayedQueue實現延遲任務掃描)
- 吧每個執行的任務模型用ExecutorService來管理,存儲正在執行的Future
- 每次掃描到超時的任務就去內存中查找這個任務的Future是否存在,如果存在則不需要吧doing的message放到pending中
- 如果需要超時機制的話,找到對應的Future并且取消當前任務的執行,并把之前執行的操作進行業務回滾/rollback,把message放到pending中
不過我并不推薦這一套方案,因為這一套方案過于復雜,本身就是不是我們用redis作為消息隊列的初衷。
總結
redis作為消息隊列是有很大的局限性的,本身作為一個以緩存/內存存儲為主的東西,只是因為某些api上的特性,我們得以實現一個簡單的隊列服務,本身我們要選擇好業務的取舍,靈活的使用redis的MQ支持,才能實現一個好的服務。
基于上述思想的代碼實踐我已經放到了github上,部分代碼還在做成中。
github地址 : https://github.com/wgd12389/redisses/