Redis實(shí)現(xiàn)消息隊(duì)列的4種方案

原文鏈接:Redis實(shí)現(xiàn)消息隊(duì)列的方案

Redis作為內(nèi)存中的數(shù)據(jù)結(jié)構(gòu)存儲(chǔ),常用作數(shù)據(jù)庫(kù)、緩存和消息代理。它支持?jǐn)?shù)據(jù)結(jié)構(gòu),如 字符串,散列,列表,集合,帶有范圍查詢的排序集(sorted sets),位圖(bitmaps),超級(jí)日志(hyperloglogs),具有半徑查詢和流的地理空間索引。Redis具有內(nèi)置復(fù)制,Lua腳本,LRU驅(qū)逐,事務(wù)和不同級(jí)別的磁盤(pán)持久性,并通過(guò)Redis Sentinel和Redis Cluster自動(dòng)分區(qū)。

為了實(shí)現(xiàn)其出色的性能,Redis使用內(nèi)存數(shù)據(jù)集(in-memory dataset)

MQ應(yīng)用有很多,比如ActiveMQ,RabbitMQ,Kafka等,但是也可以基于redis來(lái)實(shí)現(xiàn),可以降低系統(tǒng)的維護(hù)成本和實(shí)現(xiàn)復(fù)雜度,本篇介紹redis中實(shí)現(xiàn)消息隊(duì)列的幾種方案。

1. 基于List的 LPUSH+BRPOP 的實(shí)現(xiàn)

2. PUB/SUB,訂閱/發(fā)布模式

3. 基于Sorted-Set的實(shí)現(xiàn)

4. 基于Stream類型的實(shí)現(xiàn)

基于異步消息隊(duì)列List lpush-brpop(rpush-blpop)

使用rpushlpush操作入隊(duì)列,lpoprpop操作出隊(duì)列。

List支持多個(gè)生產(chǎn)者和消費(fèi)者并發(fā)進(jìn)出消息,每個(gè)消費(fèi)者拿到都是不同的列表元素。

但是當(dāng)隊(duì)列為空時(shí),lpop和rpop會(huì)一直空輪訓(xùn),消耗資源;所以引入阻塞讀blpop和brpop(b代表blocking),阻塞讀在隊(duì)列沒(méi)有數(shù)據(jù)的時(shí)候進(jìn)入休眠狀態(tài),

一旦數(shù)據(jù)到來(lái)則立刻醒過(guò)來(lái),消息延遲幾乎為零。

注意

你以為上面的方案很完美?還有個(gè)問(wèn)題需要解決:空閑連接的問(wèn)題。

如果線程一直阻塞在那里,Redis客戶端的連接就成了閑置連接,閑置過(guò)久,服務(wù)器一般會(huì)主動(dòng)斷開(kāi)連接,減少閑置資源占用,這個(gè)時(shí)候blpop和brpop或拋出異常,

所以在編寫(xiě)客戶端消費(fèi)者的時(shí)候要小心,如果捕獲到異常,還有重試。

缺點(diǎn):

做消費(fèi)者確認(rèn)ACK麻煩,不能保證消費(fèi)者消費(fèi)消息后是否成功處理的問(wèn)題(宕機(jī)或處理異常等),通常需要維護(hù)一個(gè)Pending列表,保證消息處理確認(rèn)。

不能做廣播模式,如pub/sub,消息發(fā)布/訂閱模型

不能重復(fù)消費(fèi),一旦消費(fèi)就會(huì)被刪除

不支持分組消費(fèi)

PUB/SUB,訂閱/發(fā)布模式

SUBSCRIBE,用于訂閱信道

PUBLISH,向信道發(fā)送消息

UNSUBSCRIBE,取消訂閱

此模式允許生產(chǎn)者只生產(chǎn)一次消息,由中間件負(fù)責(zé)將消息復(fù)制到多個(gè)消息隊(duì)列,每個(gè)消息隊(duì)列由對(duì)應(yīng)的消費(fèi)組消費(fèi)。

優(yōu)點(diǎn)

典型的廣播模式,一個(gè)消息可以發(fā)布到多個(gè)消費(fèi)者

多信道訂閱,消費(fèi)者可以同時(shí)訂閱多個(gè)信道,從而接收多類消息

消息即時(shí)發(fā)送,消息不用等待消費(fèi)者讀取,消費(fèi)者會(huì)自動(dòng)接收到信道發(fā)布的消息

缺點(diǎn)

消息一旦發(fā)布,不能接收。換句話就是發(fā)布時(shí)若客戶端不在線,則消息丟失,不能尋回

不能保證每個(gè)消費(fèi)者接收的時(shí)間是一致的

若消費(fèi)者客戶端出現(xiàn)消息積壓,到一定程度,會(huì)被強(qiáng)制斷開(kāi),導(dǎo)致消息意外丟失。通常發(fā)生在消息的生產(chǎn)遠(yuǎn)大于消費(fèi)速度時(shí)

可見(jiàn),Pub/Sub 模式不適合做消息存儲(chǔ),消息積壓類的業(yè)務(wù),而是擅長(zhǎng)處理廣播,即時(shí)通訊,即時(shí)反饋的業(yè)務(wù)。

基于Sorted-Set的實(shí)現(xiàn)

Sortes Set(有序列表),類似于java的SortedSet和HashMap的結(jié)合體,一方面她是一個(gè)set,保證內(nèi)部value的唯一性,另一方面它可以給每個(gè)value賦予一個(gè)score,代表這個(gè)value的

排序權(quán)重。內(nèi)部實(shí)現(xiàn)是“跳躍表”。

有序集合的方案是在自己確定消息順I(yè)D時(shí)比較常用,使用集合成員的Score來(lái)作為消息ID,保證順序,還可以保證消息ID的單調(diào)遞增。通常可以使用時(shí)間戳+序號(hào)的方案。確保了消息ID的單調(diào)遞增,利用SortedSet的依據(jù)

Score排序的特征,就可以制作一個(gè)有序的消息隊(duì)列了。

優(yōu)點(diǎn)

就是可以自定義消息ID,在消息ID有意義時(shí),比較重要。

缺點(diǎn)

缺點(diǎn)也明顯,不允許重復(fù)消息(因?yàn)槭羌希瑫r(shí)消息ID確定有錯(cuò)誤會(huì)導(dǎo)致消息的順序出錯(cuò)。

基于Stream類型的實(shí)現(xiàn)


Stream為redis 5.0后新增的數(shù)據(jù)結(jié)構(gòu)。支持多播的可持久化消息隊(duì)列,實(shí)現(xiàn)借鑒了Kafka設(shè)計(jì)。

Redis Stream的結(jié)構(gòu)如上圖所示,它有一個(gè)消息鏈表,將所有加入的消息都串起來(lái),每個(gè)消息都有一個(gè)唯一的ID和對(duì)應(yīng)的內(nèi)容消息是持久化的,Redis重啟后,內(nèi)容還在。

每個(gè)Stream都有唯一的名稱,它就是Redis的key,在我們首次使用xadd指令追加消息時(shí)自動(dòng)創(chuàng)建

每個(gè)Stream都可以掛多個(gè)消費(fèi)組,每個(gè)消費(fèi)組會(huì)有個(gè)游標(biāo)last_delivered_id在Stream數(shù)組之上往前移動(dòng),表示當(dāng)前消費(fèi)組已經(jīng)消費(fèi)到哪條消息了。每個(gè)消費(fèi)組都有一個(gè)Stream內(nèi)唯一的名稱,消費(fèi)組不會(huì)自動(dòng)創(chuàng)建,它需要單獨(dú)的指令xgroup create進(jìn)行創(chuàng)建,需要指定從Stream的某個(gè)消息ID開(kāi)始消費(fèi),這個(gè)ID用來(lái)初始化last_delivered_id變量。

每個(gè)消費(fèi)組(Consumer Group)的狀態(tài)都是獨(dú)立的,相互不受影響。也就是說(shuō)同一份Stream內(nèi)部的消息會(huì)被每個(gè)消費(fèi)組都消費(fèi)到

同一個(gè)消費(fèi)組(Consumer Group)可以掛接多個(gè)消費(fèi)者(Consumer),這些消費(fèi)者之間是競(jìng)爭(zhēng)關(guān)系,任意一個(gè)消費(fèi)者讀取了消息都會(huì)使游標(biāo)last_delivered_id往前移動(dòng)。每個(gè)消費(fèi)者者有一個(gè)組內(nèi)唯一名稱。

消費(fèi)者(Consumer)內(nèi)部會(huì)有個(gè)狀態(tài)變量pending_ids,它記錄了當(dāng)前已經(jīng)被客戶端讀取的消息,但是還沒(méi)有ack。如果客戶端沒(méi)有ack,這個(gè)變量里面的消息ID會(huì)越來(lái)越多,一旦某個(gè)消息被ack,它就開(kāi)始減少。這個(gè)pending_ids變量在Redis官方被稱之為PEL,也就是Pending Entries List,這是一個(gè)很核心的數(shù)據(jù)結(jié)構(gòu),它用來(lái)確保客戶端至少消費(fèi)了消息一次,而不會(huì)在網(wǎng)絡(luò)傳輸?shù)闹型緛G失了沒(méi)處理。

增刪改查

xadd?追加消息

xdel 刪除消息,這里的刪除僅僅是設(shè)置了標(biāo)志位,不影響消息總長(zhǎng)度

xrange?獲取消息列表,會(huì)自動(dòng)過(guò)濾已經(jīng)刪除的消息

xlen 消息長(zhǎng)度

del 刪除Stream

獨(dú)立消費(fèi)

我們可以在不定義消費(fèi)組的情況下進(jìn)行Stream消息的獨(dú)立消費(fèi),當(dāng)Stream沒(méi)有新消息時(shí),甚至可以阻塞等待。Redis設(shè)計(jì)了一個(gè)單獨(dú)的消費(fèi)指令xread,可以將Stream當(dāng)成普通的消息隊(duì)列(list)來(lái)使用。使用xread時(shí),我們可以完全忽略消費(fèi)組(Consumer Group)的存在,就好比Stream就是一個(gè)普通的列表(list)。

創(chuàng)建消費(fèi)組

Stream通過(guò)xgroup create指令創(chuàng)建消費(fèi)組(Consumer Group),需要傳遞起始消息ID參數(shù)用來(lái)初始化last_delivered_id變量。

消費(fèi)

Stream提供了xreadgroup指令可以進(jìn)行消費(fèi)組的組內(nèi)消費(fèi),需要提供消費(fèi)組名稱、消費(fèi)者名稱和起始消息ID。它同xread一樣,也可以阻塞等待新消息。讀到新消息后,對(duì)應(yīng)的消息ID就會(huì)進(jìn)入消費(fèi)者的PEL(正在處理的消息)結(jié)構(gòu)里,客戶端處理完畢后使用xack指令通知服務(wù)器,本條消息已經(jīng)處理完畢,該消息ID就會(huì)從PEL中移除。

Stream消息太多怎么辦


讀者很容易想到,要是消息積累太多,Stream的鏈表豈不是很長(zhǎng),內(nèi)容會(huì)不會(huì)爆掉就是個(gè)問(wèn)題了。xdel指令又不會(huì)刪除消息,它只是給消息做了個(gè)標(biāo)志位。

Redis自然考慮到了這一點(diǎn),所以它提供了一個(gè)定長(zhǎng)Stream功能。在xadd的指令提供一個(gè)定長(zhǎng)長(zhǎng)度maxlen,就可以將老的消息干掉,確保最多不超過(guò)指定長(zhǎng)度。

127.0.0.1:6379> xlen codehole

(integer) 5

127.0.0.1:6379> xadd codehole maxlen 3 * name xiaorui age 1

1527855160273-0

127.0.0.1:6379> xlen codehole

(integer) 3

我們看到Stream的長(zhǎng)度被砍掉了。

消息如果忘記ACK會(huì)怎樣

Stream在每個(gè)消費(fèi)者結(jié)構(gòu)中保存了正在處理中的消息ID列表PEL,如果消費(fèi)者收到了消息處理完了但是沒(méi)有回復(fù)ack,就會(huì)導(dǎo)致PEL列表不斷增長(zhǎng),如果有很多消費(fèi)組的話,那么這個(gè)PEL占用的內(nèi)存就會(huì)放大。

PEL如何避免消息丟失

在客戶端消費(fèi)者讀取Stream消息時(shí),Redis服務(wù)器將消息回復(fù)給客戶端的過(guò)程中,客戶端突然斷開(kāi)了連接,消息就丟失了。但是PEL里已經(jīng)保存了發(fā)出去的消息ID。待客戶端重新連上之后,可以再次收到PEL中的消息ID列表。不過(guò)此時(shí)xreadgroup的起始消息必須是任意有效的消息ID,一般將參數(shù)設(shè)為0-0,表示讀取所有的PEL消息以及自last_delivered_id之后的新消息。

分區(qū)Partition

Redis沒(méi)有原生支持分區(qū)的能力,想要使用分區(qū),需要分配多個(gè)Stream,然后在客戶端使用一定的策略來(lái)講消息放入不同的stream。

結(jié)論

Stream的消費(fèi)模型借鑒了kafka的消費(fèi)分組的概念,它彌補(bǔ)了Redis Pub/Sub不能持久化消息的缺陷。但是它又不同于kafka,kafka的消息可以分partition,而Stream不行。如果非要分parition的話,得在客戶端做,提供不同的Stream名稱,對(duì)消息進(jìn)行hash取模來(lái)選擇往哪個(gè)Stream里塞。如果讀者稍微研究過(guò)Redis作者的另一個(gè)開(kāi)源項(xiàng)目Disque的話,這極可能是作者意識(shí)到Disque項(xiàng)目的活躍程度不夠,所以將Disque的內(nèi)容移植到了Redis里面。這只是本人的猜測(cè),未必是作者的初衷。如果讀者有什么不同的想法,可以在評(píng)論區(qū)一起參與討論。

參考文章:

https://blog.csdn.net/enmotech/article/details/81230531

http://www.hellokang.net/redis/message-queue-by-redis.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容