­

Redis(8)——發佈/訂閱與Stream

  • 2020 年 3 月 15 日
  • 筆記

一、Redis 中的發佈/訂閱功能

發佈/ 訂閱系統 是 Web 系統中比較常用的一個功能。簡單點說就是 發佈者發佈消息,訂閱者接受消息,這有點類似於我們的報紙/ 雜誌社之類的: (借用前邊的一張圖)

從我們 前面(下方相關閱讀) 學習的知識來看,我們雖然可以使用一個 list 列表結構結合 lpushrpop 來實現消息隊列的功能,但是似乎很難實現實現 消息多播 的功能:

為了支持消息多播,Redis 不能再依賴於那 5 種基礎的數據結構了,它單獨使用了一個模塊來支持消息多播,這個模塊就是 PubSub,也就是 PublisherSubscriber (發佈者/ 訂閱者模式)

PubSub 簡介

我們從 上面的圖 中可以看到,基於 list 結構的消息隊列,是一種 PublisherConsumer 點對點的強關聯關係,Redis 為了消除這樣的強關聯,引入了另一種概念:頻道 (channel)

Publisherchannel 中發佈消息時,關注了指定 channelConsumer 就能夠同時受到消息。但這裡的 問題 是,消費者訂閱一個頻道是必須 明確指定頻道名稱 的,這意味着,如果我們想要 訂閱多個 頻道,那麼就必須 顯式地關注多個 名稱。

為了簡化訂閱的繁瑣操作,Redis 提供了 模式訂閱 的功能 Pattern Subscribe,這樣就可以 一次性關注多個頻道 了,即使生產者新增了同模式的頻道,消費者也可以立即受到消息:

例如上圖中,所有 位於圖片下方的 Consumer 都能夠受到消息

Publisherwmyskxz.chat 這個 channel 中發送了一條消息,不僅僅關注了這個頻道的 Consumer 1Consumer 2 能夠受到消息,圖片中的兩個 channel 都和模式 wmyskxz.* 匹配,所以 Redis 此時會同樣發送消息給訂閱了 wmyskxz.* 這個模式的 Consumer 3 和關注了在這個模式下的另一個頻道 wmyskxz.log 下的 Consumer 4Consumer 5

另一方面,如果接收消息的頻道是 wmyskxz.chat,那麼 Consumer 3 也會受到消息。

快速體驗

Redis 中,PubSub 模塊的使用非常簡單,常用的命令也就下面這麼幾條:

# 訂閱頻道:  SUBSCRIBE channel [channel ....]   # 訂閱給定的一個或多個頻道的信息  PSUBSCRIBE pattern [pattern ....]  # 訂閱一個或多個符合給定模式的頻道  # 發佈頻道:  PUBLISH channel message  # 將消息發送到指定的頻道  # 退訂頻道:  UNSUBSCRIBE [channel [channel ....]]   # 退訂指定的頻道  PUNSUBSCRIBE [pattern [pattern ....]]  #退訂所有給定模式的頻道

我們可以在本地快速地來體驗一下 PubSub

具體步驟如下:

  1. 開啟本地 Redis 服務,新建兩個控制台窗口;
  2. 在其中一個窗口輸入 SUBSCRIBE wmyskxz.chat 關注 wmyskxz.chat 頻道,讓這個窗口成為 消費者
  3. 在另一個窗口輸入 PUBLISH wmyskxz.chat 'message' 往這個頻道發送消息,這個時候就會看到 另一個窗口實時地出現 了發送的測試消息。

實現原理

可以看到,我們通過很簡單的兩條命令,幾乎就可以簡單使用這樣的一個 發佈/ 訂閱系統 了,但是具體是怎麼樣實現的呢?

每個 Redis 服務器進程維持着一個標識服務器狀態redis.h/redisServer 結構,其中就 保存着有訂閱的頻道 以及 訂閱模式 的信息:

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

訂閱頻道原理

當客戶端訂閱某一個頻道之後,Redis 就會往 pubsub_channels 這個字典中新添加一條數據,實際上這個 dict 字典維護的是一張鏈表,比如,下圖展示的 pubsub_channels 示例中,client 1client 2 就訂閱了 channel 1,而其他頻道也分別被其他客戶端訂閱:

SUBSCRIBE 命令

SUBSCRIBE 命令的行為可以用下列的偽代碼表示:

def SUBSCRIBE(client, channels):      # 遍歷所有輸入頻道      for channel in channels:          # 將客戶端添加到鏈表的末尾          redisServer.pubsub_channels[channel].append(client)

通過 pubsub_channels 字典,程序只要檢查某個頻道是否為字典的鍵,就可以知道該頻道是否正在被客戶端訂閱;只要取出某個鍵的值,就可以得到所有訂閱該頻道的客戶端的信息。

PUBLISH 命令

了解 SUBSCRIBE,那麼 PUBLISH 命令的實現也變得十分簡單了,只需要通過上述字典定位到具體的客戶端,再把消息發送給它們就好了:(偽代碼實現如下)

def PUBLISH(channel, message):      # 遍歷所有訂閱頻道 channel 的客戶端      for client in server.pubsub_channels[channel]:          # 將信息發送給它們          send_message(client, message)

UNSUBSCRIBE 命令

使用 UNSUBSCRIBE 命令可以退訂指定的頻道,這個命令執行的是訂閱的反操作:它從 pubsub_channels 字典的給定頻道(鍵)中,刪除關於當前客戶端的信息,這樣被退訂頻道的信息就不會再發送給這個客戶端。

訂閱模式原理

正如我們上面說到了,當發送一條消息到 wmyskxz.chat 這個頻道時,Redis 不僅僅會發送到當前的頻道,還會發送到匹配於當前模式的所有頻道,實際上,pubsub_patterns 背後還維護了一個 redis.h/pubsubPattern 結構:

typedef struct pubsubPattern {      redisClient *client;  // 訂閱模式的客戶端      robj *pattern;        // 訂閱的模式  } pubsubPattern;

每當調用 PSUBSCRIBE 命令訂閱一個模式時,程序就創建一個包含客戶端信息和被訂閱模式的 pubsubPattern 結構,並將該結構添加到 redisServer.pubsub_patterns 鏈表中。

我們來看一個 pusub_patterns 鏈表的示例:

這個時候客戶端 client 3 執行 PSUBSCRIBE wmyskxz.java.*,那麼 pubsub_patterns 鏈表就會被更新成這樣:

通過遍歷整個 pubsub_patterns 鏈表,程序可以檢查所有正在被訂閱的模式,以及訂閱這些模式的客戶端。

PUBLISH 命令

上面給出的偽代碼並沒有 完整描述 PUBLISH 命令的行為,因為 PUBLISH 除了將 message 發送到 所有訂閱 channel 的客戶端 之外,它還會將 channelpubsub_patterns 中的 模式 進行對比,如果 channel 和某個模式匹配的話,那麼也將 message 發送到 訂閱那個模式的客戶端

完整描述 PUBLISH 功能的偽代碼定於如下:

def PUBLISH(channel, message):      # 遍歷所有訂閱頻道 channel 的客戶端      for client in server.pubsub_channels[channel]:          # 將信息發送給它們          send_message(client, message)      # 取出所有模式,以及訂閱模式的客戶端      for pattern, client in server.pubsub_patterns:          # 如果 channel 和模式匹配          if match(channel, pattern):              # 那麼也將信息發給訂閱這個模式的客戶端              send_message(client, message)

PUNSUBSCRIBE 命令

使用 PUNSUBSCRIBE 命令可以退訂指定的模式,這個命令執行的是訂閱模式的反操作:序會刪除 redisServer.pubsub_patterns 鏈表中,所有和被退訂模式相關聯的 pubsubPattern 結構,這樣客戶端就不會再收到和模式相匹配的頻道發來的信息。

PubSub 的缺點

儘管 Redis 實現了 PubSub 模式來達到了 多播消息隊列 的目的,但在實際的消息隊列的領域,幾乎 找不到特別合適的場景,因為它的缺點十分明顯:

  • 沒有 Ack 機制,也不保證數據的連續: PubSub 的生產者傳遞過來一個消息,Redis 會直接找到相應的消費者傳遞過去。如果沒有一個消費者,那麼消息會被直接丟棄。如果開始有三個消費者,其中一個突然掛掉了,過了一會兒等它再重連時,那麼重連期間的消息對於這個消費者來說就徹底丟失了。
  • 不持久化消息: 如果 Redis 停機重啟,PubSub 的消息是不會持久化的,畢竟 Redis 宕機就相當於一個消費者都沒有,所有的消息都會被直接丟棄。

基於上述缺點,Redis 的作者甚至單獨開啟了一個 Disque 的項目來專門用來做多播消息隊列,不過該項目目前好像都沒有成熟。不過後來在 2018 年 6 月,Redis 5.0 新增了 Stream 數據結構,這個功能給 Redis 帶來了 持久化消息隊列,從此 PubSub 作為消息隊列的功能可以說是就消失了..

二、更為強大的 Stream | 持久化的發佈/訂閱系統

Redis Stream 從概念上來說,就像是一個 僅追加內容消息鏈表,把所有加入的消息都一個一個串起來,每個消息都有一個唯一的 ID 和內容,這很簡單,讓它複雜的是從 Kafka 借鑒的另一種概念:消費者組(Consumer Group) (思路一致,實現不同)

上圖就展示了一個典型的 Stream 結構。每個 Stream 都有唯一的名稱,它就是 Redis 的 key,在我們首次使用 xadd 指令追加消息時自動創建。我們對圖中的一些概念做一下解釋:

  • Consumer Group:消費者組,可以簡單看成記錄流狀態的一種數據結構。消費者既可以選擇使用 XREAD 命令進行 獨立消費,也可以多個消費者同時加入一個消費者組進行 組內消費。同一個消費者組內的消費者共享所有的 Stream 信息,同一條消息只會有一個消費者消費到,這樣就可以應用在分佈式的應用場景中來保證消息的唯一性。
  • last_delivered_id:用來表示消費者組消費在 Stream 上 消費位置 的游標信息。每個消費者組都有一個 Stream 內 唯一的名稱,消費者組不會自動創建,需要使用 XGROUP CREATE 指令來顯式創建,並且需要指定從哪一個消息 ID 開始消費,用來初始化 last_delivered_id 這個變量。
  • pending_ids:每個消費者內部都有的一個狀態變量,用來表示 已經 被客戶端 獲取,但是 還沒有 ack 的消息。記錄的目的是為了 保證客戶端至少消費了消息一次,而不會在網絡傳輸的中途丟失而沒有對消息進行處理。如果客戶端沒有 ack,那麼這個變量裏面的消息 ID 就會越來越多,一旦某個消息被 ack,它就會對應開始減少。這個變量也被 Redis 官方稱為 PEL (Pending Entries List)

消息 ID 和消息內容

消息 ID

消息 ID 如果是由 XADD 命令返回自動創建的話,那麼它的格式會像這樣:timestampInMillis-sequence (毫秒時間戳-序列號),例如 1527846880585-5,它表示當前的消息是在毫秒時間戳 1527846880585 時產生的,並且是該毫秒內產生的第 5 條消息。

這些 ID 的格式看起來有一些奇怪,為什麼要使用時間來當做 ID 的一部分呢? 一方面,我們要 滿足 ID 自增 的屬性,另一方面,也是為了 支持範圍查找 的功能。由於 ID 和生成消息的時間有關,這樣就使得在根據時間範圍內查找時基本上是沒有額外損耗的。

當然消息 ID 也可以由客戶端自定義,但是形式必須是 "整數-整數",而且後面加入的消息的 ID 必須要大於前面的消息 ID。

消息內容

消息內容就是普通的鍵值對,形如 hash 結構的鍵值對。

增刪改查示例

增刪改查命令很簡單,詳情如下:

  1. xadd:追加消息
  2. xdel:刪除消息,這裡的刪除僅僅是設置了標誌位,不影響消息總長度
  3. xrange:獲取消息列表,會自動過濾已經刪除的消息
  4. xlen:消息長度
  5. del:刪除Stream

使用示例:

# *號表示服務器自動生成ID,後面順序跟着一堆key/value  127.0.0.1:6379> xadd codehole * name laoqian age 30  #  名字叫laoqian,年齡30歲  1527849609889-0  # 生成的消息ID  127.0.0.1:6379> xadd codehole * name xiaoyu age 29  1527849629172-0  127.0.0.1:6379> xadd codehole * name xiaoqian age 1  1527849637634-0  127.0.0.1:6379> xlen codehole  (integer) 3  127.0.0.1:6379> xrange codehole - +  # -表示最小值, +表示最大值  1) 1) 1527849609889-0     2) 1) "name"        2) "laoqian"        3) "age"        4) "30"  2) 1) 1527849629172-0     2) 1) "name"        2) "xiaoyu"        3) "age"        4) "29"  3) 1) 1527849637634-0     2) 1) "name"        2) "xiaoqian"        3) "age"        4) "1"  127.0.0.1:6379> xrange codehole 1527849629172-0 +  # 指定最小消息ID的列表  1) 1) 1527849629172-0     2) 1) "name"        2) "xiaoyu"        3) "age"        4) "29"  2) 1) 1527849637634-0     2) 1) "name"        2) "xiaoqian"        3) "age"        4) "1"  127.0.0.1:6379> xrange codehole - 1527849629172-0  # 指定最大消息ID的列表  1) 1) 1527849609889-0     2) 1) "name"        2) "laoqian"        3) "age"        4) "30"  2) 1) 1527849629172-0     2) 1) "name"        2) "xiaoyu"        3) "age"        4) "29"  127.0.0.1:6379> xdel codehole 1527849609889-0  (integer) 1  127.0.0.1:6379> xlen codehole  # 長度不受影響  (integer) 3  127.0.0.1:6379> xrange codehole - +  # 被刪除的消息沒了  1) 1) 1527849629172-0     2) 1) "name"        2) "xiaoyu"        3) "age"        4) "29"  2) 1) 1527849637634-0     2) 1) "name"        2) "xiaoqian"        3) "age"        4) "1"  127.0.0.1:6379> del codehole  # 刪除整個Stream  (integer) 1

獨立消費示例

我們可以在不定義消費組的情況下進行 Stream 消息的 獨立消費,當 Stream 沒有新消息時,甚至可以阻塞等待。Redis 設計了一個單獨的消費指令 xread,可以將 Stream 當成普通的消息隊列(list)來使用。使用 xread 時,我們可以完全忽略 消費組(Consumer Group) 的存在,就好比 Stream 就是一個普通的列表(list):

# 從Stream頭部讀取兩條消息  127.0.0.1:6379> xread count 2 streams codehole 0-0  1) 1) "codehole"     2) 1) 1) 1527851486781-0           2) 1) "name"              2) "laoqian"              3) "age"              4) "30"        2) 1) 1527851493405-0           2) 1) "name"              2) "yurui"              3) "age"              4) "29"  # 從Stream尾部讀取一條消息,毫無疑問,這裡不會返回任何消息  127.0.0.1:6379> xread count 1 streams codehole $  (nil)  # 從尾部阻塞等待新消息到來,下面的指令會堵住,直到新消息到來  127.0.0.1:6379> xread block 0 count 1 streams codehole $  # 我們從新打開一個窗口,在這個窗口往Stream里塞消息  127.0.0.1:6379> xadd codehole * name youming age 60  1527852774092-0  # 再切換到前面的窗口,我們可以看到阻塞解除了,返回了新的消息內容  # 而且還顯示了一個等待時間,這裡我們等待了93s  127.0.0.1:6379> xread block 0 count 1 streams codehole $  1) 1) "codehole"     2) 1) 1) 1527852774092-0           2) 1) "name"              2) "youming"              3) "age"              4) "60"  (93.11s)

客戶端如果想要使用 xread 進行 順序消費,一定要 記住當前消費 到哪裡了,也就是返回的消息 ID。下次繼續調用 xread 時,將上次返回的最後一個消息 ID 作為參數傳遞進去,就可以繼續消費後續的消息。

block 0 表示永遠阻塞,直到消息到來,block 1000 表示阻塞 1s,如果 1s 內沒有任何消息到來,就返回 nil

127.0.0.1:6379> xread block 1000 count 1 streams codehole $  (nil)  (1.07s)

創建消費者示例

Stream 通過 xgroup create 指令創建消費組(Consumer Group),需要傳遞起始消息 ID 參數用來初始化 last_delivered_id 變量:

127.0.0.1:6379> xgroup create codehole cg1 0-0  #  表示從頭開始消費  OK  # $表示從尾部開始消費,只接受新消息,當前Stream消息會全部忽略  127.0.0.1:6379> xgroup create codehole cg2 $  OK  127.0.0.1:6379> xinfo codehole  # 獲取Stream信息   1) length   2) (integer) 3  # 共3個消息   3) radix-tree-keys   4) (integer) 1   5) radix-tree-nodes   6) (integer) 2   7) groups   8) (integer) 2  # 兩個消費組   9) first-entry  # 第一個消息  10) 1) 1527851486781-0      2) 1) "name"         2) "laoqian"         3) "age"         4) "30"  11) last-entry  # 最後一個消息  12) 1) 1527851498956-0      2) 1) "name"         2) "xiaoqian"         3) "age"         4) "1"  127.0.0.1:6379> xinfo groups codehole  # 獲取Stream的消費組信息  1) 1) name     2) "cg1"     3) consumers     4) (integer) 0  # 該消費組還沒有消費者     5) pending     6) (integer) 0  # 該消費組沒有正在處理的消息  2) 1) name     2) "cg2"     3) consumers  # 該消費組還沒有消費者     4) (integer) 0     5) pending     6) (integer) 0  # 該消費組沒有正在處理的消息

組內消費示例

Stream 提供了 xreadgroup 指令可以進行消費組的組內消費,需要提供 消費組名稱、消費者名稱和起始消息 ID。它同 xread 一樣,也可以阻塞等待新消息。讀到新消息後,對應的消息 ID 就會進入消費者的 PEL (正在處理的消息) 結構里,客戶端處理完畢後使用 xack 指令 通知服務器,本條消息已經處理完畢,該消息 ID 就會從 PEL 中移除,下面是示例:

# >號表示從當前消費組的last_delivered_id後面開始讀  # 每當消費者讀取一條消息,last_delivered_id變量就會前進  127.0.0.1:6379> xreadgroup GROUP cg1 c1 count 1 streams codehole >  1) 1) "codehole"     2) 1) 1) 1527851486781-0           2) 1) "name"              2) "laoqian"              3) "age"              4) "30"  127.0.0.1:6379> xreadgroup GROUP cg1 c1 count 1 streams codehole >  1) 1) "codehole"     2) 1) 1) 1527851493405-0           2) 1) "name"              2) "yurui"              3) "age"              4) "29"  127.0.0.1:6379> xreadgroup GROUP cg1 c1 count 2 streams codehole >  1) 1) "codehole"     2) 1) 1) 1527851498956-0           2) 1) "name"              2) "xiaoqian"              3) "age"              4) "1"        2) 1) 1527852774092-0           2) 1) "name"              2) "youming"              3) "age"              4) "60"  # 再繼續讀取,就沒有新消息了  127.0.0.1:6379> xreadgroup GROUP cg1 c1 count 1 streams codehole >  (nil)  # 那就阻塞等待吧  127.0.0.1:6379> xreadgroup GROUP cg1 c1 block 0 count 1 streams codehole >  # 開啟另一個窗口,往裡塞消息  127.0.0.1:6379> xadd codehole * name lanying age 61  1527854062442-0  # 回到前一個窗口,發現阻塞解除,收到新消息了  127.0.0.1:6379> xreadgroup GROUP cg1 c1 block 0 count 1 streams codehole >  1) 1) "codehole"     2) 1) 1) 1527854062442-0           2) 1) "name"              2) "lanying"              3) "age"              4) "61"  (36.54s)  127.0.0.1:6379> xinfo groups codehole  # 觀察消費組信息  1) 1) name     2) "cg1"     3) consumers     4) (integer) 1  # 一個消費者     5) pending     6) (integer) 5  # 共5條正在處理的信息還有沒有ack  2) 1) name     2) "cg2"     3) consumers     4) (integer) 0  # 消費組cg2沒有任何變化,因為前面我們一直在操縱cg1     5) pending     6) (integer) 0  # 如果同一個消費組有多個消費者,我們可以通過xinfo consumers指令觀察每個消費者的狀態  127.0.0.1:6379> xinfo consumers codehole cg1  # 目前還有1個消費者  1) 1) name     2) "c1"     3) pending     4) (integer) 5  # 共5條待處理消息     5) idle     6) (integer) 418715  # 空閑了多長時間ms沒有讀取消息了  # 接下來我們ack一條消息  127.0.0.1:6379> xack codehole cg1 1527851486781-0  (integer) 1  127.0.0.1:6379> xinfo consumers codehole cg1  1) 1) name     2) "c1"     3) pending     4) (integer) 4  # 變成了5條     5) idle     6) (integer) 668504  # 下面ack所有消息  127.0.0.1:6379> xack codehole cg1 1527851493405-0 1527851498956-0 1527852774092-0 1527854062442-0  (integer) 4  127.0.0.1:6379> xinfo consumers codehole cg1  1) 1) name     2) "c1"     3) pending     4) (integer) 0  # pel空了     5) idle     6) (integer) 745505

QA 1:Stream 消息太多怎麼辦? | Stream 的上限

很容易想到,要是消息積累太多,Stream 的鏈表豈不是很長,內容會不會爆掉就是個問題了。xdel 指令又不會刪除消息,它只是給消息做了個標誌位。

Redis 自然考慮到了這一點,所以它提供了一個定長 Stream 功能。在 xadd 的指令提供一個定長長度 maxlen,就可以將老的消息幹掉,確保最多不超過指定長度,使用起來也很簡單:

> XADD mystream MAXLEN 2 * value 1  1526654998691-0  > XADD mystream MAXLEN 2 * value 2  1526654999635-0  > XADD mystream MAXLEN 2 * value 3  1526655000369-0  > XLEN mystream  (integer) 2  > XRANGE mystream - +  1) 1) 1526654999635-0     2) 1) "value"        2) "2"  2) 1) 1526655000369-0     2) 1) "value"        2) "3"

如果使用 MAXLEN 選項,當 Stream 的達到指定長度後,老的消息會自動被淘汰掉,因此 Stream 的大小是恆定的。目前還沒有選項讓 Stream 只保留給定數量的條目,因為為了一致地運行,這樣的命令必須在很長一段時間內阻塞以淘汰消息。(例如在添加數據的高峰期間,你不得不長暫停來淘汰舊消息和添加新的消息)

另外使用 MAXLEN 選項的花銷是很大的,Stream 為了節省內存空間,採用了一種特殊的結構表示,而這種結構的調整是需要額外的花銷的。所以我們可以使用一種帶有 ~ 的特殊命令:

XADD mystream MAXLEN ~ 1000 * ... entry fields here ...

它會基於當前的結構合理地對節點執行裁剪,來保證至少會有 1000 條數據,可能是 1010 也可能是 1030

QA 2:PEL 是如何避免消息丟失的?

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

Redis Stream Vs Kafka

Redis 基於內存存儲,這意味着它會比基於磁盤的 Kafka 快上一些,也意味着使用 Redis 我們 不能長時間存儲大量數據。不過如果您想以 最小延遲 實時處理消息的話,您可以考慮 Redis,但是如果 消息很大並且應該重用數據 的話,則應該首先考慮使用 Kafka。

另外從某些角度來說,Redis Stream 也更適用於小型、廉價的應用程序,因為 Kafka 相對來說更難配置一些。

相關閱讀

  1. Redis(1)——5種基本數據結構 – https://www.wmyskxz.com/2020/02/28/redis-1-5-chong-ji-ben-shu-ju-jie-gou/
  2. Redis(2)——跳躍表 – https://www.wmyskxz.com/2020/02/29/redis-2-tiao-yue-biao/
  3. Redis(3)——分佈式鎖深入探究 – https://www.wmyskxz.com/2020/03/01/redis-3/
  4. Reids(4)——神奇的HyperLoglog解決統計問題 – https://www.wmyskxz.com/2020/03/02/reids-4-shen-qi-de-hyperloglog-jie-jue-tong-ji-wen-ti/
  5. Redis(5)——億級數據過濾和布隆過濾器 – https://www.wmyskxz.com/2020/03/11/redis-5-yi-ji-shu-ju-guo-lu-he-bu-long-guo-lu-qi/
  6. Redis(6)——GeoHash查找附近的人https://www.wmyskxz.com/2020/03/12/redis-6-geohash-cha-zhao-fu-jin-de-ren/
  7. Redis(7)——持久化【一文了解】 – https://www.wmyskxz.com/2020/03/13/redis-7-chi-jiu-hua-yi-wen-liao-jie/

參考資料

  1. 訂閱與發佈——Redis 設計與實現 – https://redisbook.readthedocs.io/en/latest/feature/pubsub.html
  2. 《Redis 深度歷險》 – 錢文品/ 著 – https://book.douban.com/subject/30386804/
  3. Introduction to Redis Streams【官方文檔】 – https://redis.io/topics/streams-intro
  4. Kafka vs. Redis: Log Aggregation Capabilities and Performance – https://logz.io/blog/kafka-vs-redis/
  • 本文已收錄至我的 Github 程序員成長系列 【More Than Java】,學習,不止 Code,歡迎 star:https://github.com/wmyskxz/MoreThanJava
  • 個人公眾號 :wmyskxz,個人獨立域名博客:wmyskxz.com,堅持原創輸出,下方掃碼關注,2020,與您共同成長!

非常感謝各位人才能 看到這裡,如果覺得本篇文章寫得不錯,覺得 「我沒有三顆心臟」有點東西 的話,求點贊,求關注,求分享,求留言!

創作不易,各位的支持和認可,就是我創作的最大動力,我們下篇文章見!