天天看點

Redis Pub/Sub 釋出訂閱模式的深度解析與實作消息隊列1 Pub/Sub的概述2 訂閱3 取消訂閱4 模式比對5 釋出6 Pub/Sub原理7 Pub/Sub缺點

詳細介紹了Redis 的Pub/Sub的相關指令和優缺點,以及如何實作簡單的消息隊列。

文章目錄

  • 1 Pub/Sub的概述
  • 2 訂閱
  • 3 取消訂閱
  • 4 模式比對
  • 5 釋出
  • 6 Pub/Sub原理
    • 6.1 pubsub_channels
    • 6.2 pubsub_patterns
  • 7 Pub/Sub缺點

1 Pub/Sub的概述

我們可以利用Redis的List資料結構實作一

個簡單的消息隊列,通過lpush指令寫入消息,通過rpop 指令拉取消息,也可以使用BRPOP實作阻塞式的拉取消息。

上面的消息隊列有一個缺點,那就是不支援消息多點傳播機制,消息多點傳播機制就是生産者生産的一個消息可以被多個消費者消費到,這個功能在分布式系統中非常重要。

Redis單獨使用Pub/Sub子產品來支援消息多點傳播,即釋出/訂閱模式(publish/subscribe),它是一種消息通信模式:釋出者(pub)發送消息,訂閱者(sub)接收消息。

釋出者會将的消息釋出到一個chanel(通道)中而不是發送給指定的訂閱者,釋出者也不知道可能有哪些訂閱者。

訂閱者可以訂閱一個或多個channel,隻接收來自訂閱的channel的消息,并且不知道有哪些(如果有)釋出者,這種模式實作了消息釋出者和訂閱者的解耦。

Pub/Sub 與鍵空間無關,消息不會被持久化,與資料庫也無關,在db10上釋出,将可以被 db1 上的訂閱者聽到。如果我們需要某種範圍的範圍,那麼隻能在設定的channel名字上做區分。

2 訂閱

用戶端使用

SUBSCRIBE channel [channel ...]

指令訂閱通道,可以多次執行該指令,也可以一次訂閱多個通道,多個用戶端可以訂閱相同的通道。

該指令傳回一個數組,包括三部分,依次是:指令名稱(字元串“subscribe”),訂閱的通道名稱,目前總共訂閱的通道數(包含glob通道)。這三個部分對每一個訂閱的通道是連續的。

用戶端執行訂閱以後,除了可以繼續訂閱(

SUBSCRIBE

或者

PSUBSCRIBE

),取消訂閱(

UNSUBSCRIBE

或者

PUNSUBSCRIBE

),

PING

指令和結束連接配接(QUIT)外, 不能執行其他操作,用戶端将阻塞直到訂閱通道上釋出消息的到來。

如下,表示用戶端一次性訂閱四個通道:aaa、bba、ccc、ddd:

127.0.0.1:6379> SUBSCRIBE aaa bba ccc ddd
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "aaa"
3) (integer) 1
1) "subscribe"
2) "bba"
3) (integer) 2
1) "subscribe"
2) "ccc"
3) (integer) 3
1) "subscribe"
2) "ddd"
3) (integer) 4
           

請注意,如果使用

redis-cli

一旦進入訂閱模式就不會接受任何指令,隻能使用 Ctrl-C 退出該模式。

3 取消訂閱

用戶端使用

UNSUBSCRIBE [channel [channel ...]]

指令取消訂閱指定的通道,可以指定一個或者多個取消的訂閱通道名稱,也可以不帶任何參數,此時将取消所有的訂閱的通道(不包括glob通道)。

該指令傳回一個數組,包括三部分,依次是:指令名稱(字元串“unsubscribe”),訂閱的通道名稱,目前總共訂閱的通道數(包含glob通道)。這三個部分對每一個取消訂閱的通道是連續的。當最後一個參數為零時,我們不再訂閱任何頻道,用戶端可以發出任何類型的 Redis 指令,因為我們處于 Pub/Sub 狀态之外。

如下,表示用戶端退出ccccc通道的訂閱:

127.0.0.1:6379> UNSUBSCRIBE ccccc
1) "unsubscribe"
2) "cccc"
3) (integer) 0
           

4 模式比對

Redis Pub/Sub 實作支援模式比對。用戶端可以訂閱 glob 通道,這樣就能接收發送到通道名稱與給定模式比對的通道的所有消息。

用戶端使用

PSUBSCRIBE pattern [pattern ...]

訂閱一個或多個glob 通道。

該指令傳回一個數組,包括三部分,依次是:指令名稱(字元串“psubscribe”),訂閱的glob通道名稱,目前總共訂閱的通道數(包含非glob通道)。這三個部分對每一個訂閱的通道是連續的。

例如,訂閱a*和*c模式:

127.0.0.1:6379> PSUBSCRIBE a* *c
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "a*"
3) (integer) 1
1) "psubscribe"
2) "*c"
3) (integer) 2
           

用戶端使用

PUNSUBSCRIBE [pattern [pattern ...]]

退訂一個或多個glob 通道。也可以不帶任何參數,此時将取消所有的訂閱的通道(不包括非glob通道)。

該指令傳回一個數組,包括三部分,依次是:指令名稱(字元串“punsubscribe”),取消訂閱的glob通道名稱,目前總共訂閱的通道數(包含非glob通道)。這三個部分對每一個取消訂閱的通道是連續的。當最後一個參數為零時,我們不再訂閱任何頻道,用戶端可以發出任何類型的 Redis 指令,因為我們處于 Pub/Sub 狀态之外。

如下,取消對a*的glob通道的訂閱:

127.0.0.1:6379> PUNSUBSCRIBE a*
1) "punsubscribe"
2) "a*"
3) (integer) 0
           

subscribe, unsubscribe, psubscribe 和punsubscribe

指令的最後都傳回目前用戶端訂閱的glob通道和通道的總數,如果為0,則用戶端自動退出Pub/Sub模式。

5 釋出

PUBLISH channel message

指令在指定的通道上釋出消息。隻能在一個通道上釋出消息,不能在多個通道上同時釋出消息。

将傳回通知的接收者數量。這裡的接收者數目大于等于訂閱該通道的用戶端數目,因為一個用戶端的glob通道和非glob通道同時比對釋出通道的話,則視為兩個接收者。換句話說,如果用戶端訂閱了多個與已釋出消息比對的模式,或者訂閱了與該消息比對的模式和通道,則該用戶端可能會多次收到同一條消息。

在接收端,收到的響應包括三部分,依次是:“message”字元串,比對的通道名稱,釋出的消息内容。如果是因為glob模式比對而接收,那麼傳回四部分:“pmessage”字元串,比對的glob通道名稱,發送的原始通道名稱,釋出的消息内容。

如果某個用戶端的訂閱a*和*c兩個模式通道:

127.0.0.1:6379> PSUBSCRIBE a* *c
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "a*"
3) (integer) 1
1) "psubscribe"
2) "*c"
3) (integer) 2
           

如果發送消息的通道為ac,那麼将會傳回2:

127.0.0.1:6379> PUBLISH ac xxxxx
(integer) 2
           

在用戶端,将收到兩次消息:

1) "pmessage"
2) "a*"
3) "ac"
4) "xxxxx"
1) "pmessage"
2) "*c"
3) "ac"
4) "xxxxx"
           

6 Pub/Sub原理

每個Redis伺服器程序維持着一個辨別伺服器狀态的

redis.h/redisServer

結構,其中就儲存着有訂閱的頻道 以及 訂閱模式 的資訊:

struct redisServer {
    // ...
    dict *pubsub_channels;  // 訂閱頻道
    list *pubsub_patterns;  // 訂閱模式
    // ...
};
           

6.1 pubsub_channels

pubsub_channels

是一個dict字典結構,key(數組元素)為channel,value就是某個client。當用戶端訂閱某一個頻道之後,Redis 就會往 pubsub_channels 這個字典中新添加一條channel和client資料,不同的client可以訂閱相同的channel,client以連結清單的方式串聯起來,這樣就能儲存多個client對同一個channel的關系,非常的巧妙。

Redis Pub/Sub 釋出訂閱模式的深度解析與實作消息隊列1 Pub/Sub的概述2 訂閱3 取消訂閱4 模式比對5 釋出6 Pub/Sub原理7 Pub/Sub缺點

了解了這個結構,SUBSCRIBE 、PUBLISH 、UNSUBSCRIBE指令的實作也變得十分簡單了。

SUBSCRIBE

就是将channel和client加入到dict中,如果此前沒有該channel,那就新增一個channel元素,然後在再增一個client連結清單節點,如果此前存在,則直接在連結清單末尾添加一個client節點。

PUBLISH

隻需要通過上述字典定位到具體的channel,就能找到所有訂閱該channel的用戶端,再把消息發送給它們就好了。

UNSUBSCRIBE

也很簡單,将對應channel下面的連結清單中的client删除即可。

6.2 pubsub_patterns

pubsub_patterns

用于存儲所有的glob channel,它是一個list結構,節點類型為

redis.h/pubsubPattern

typedefstruct pubsubPattern {
    redisClient *client;  // 訂閱模式的用戶端
    robj *pattern;        // 訂閱的模式
} pubsubPattern;
           

當使用

PSUBSCRIBE

指令訂閱一個模式時,程式就建立一個

pubsubPattern

添加到

pubsub_patterns

連結清單中。如果另一個用戶端也訂閱一個模式,則向連結清單的後面新增一個pubsubPattern節點即可。

是以,實際上PUBLISH除了會在pubsub_channels中定位具體的channel之外,還會将指定的channel與pubsub_patterns 中的模式進行對比,如果 指定的channel 和某個模式比對的話,那麼也将 message 發送到訂閱那個模式的全部用戶端。

PUNSUBSCRIBE

的實作也很簡單,就是删除pubsub_patterns中,client和pattern資訊對比一緻的節點。

7 Pub/Sub缺點

釋出的消息在Redis系統中不能持久化,是以,必須先執行訂閱,再等待消息釋出。如果先釋出了消息,那麼該消息由于沒有訂閱者,消息将被直接丢棄。

消息隻管發送,不管接收,也沒有ACK機制,無法保證消息的消費成功。如果某個消費者中途加入進來,或者挂掉重新開機,那麼這之前丢失的消息也不能再次消費。

以上的缺點導緻Redis的Pub/Sub模式就像個小玩具,在生産環境中幾乎無用武之地,非常的尴尬!為此,Redis5.0版本新增了Stream資料結構,不但支援多點傳播,還支援資料持久化,相比Pub/Sub更加的強大!

相關文章:

  1. https://redis.io
如有需要交流,或者文章有誤,請直接留言。另外希望點贊、收藏、關注,我将不間斷更新各種Java學習部落格!

繼續閱讀