聊聊 Redis Stream

勇哥编程游记發表於2024-04-15

Redis Stream 是 Redis 5.0 版本中引入的一種新的資料結構,它用於實現簡單但功能強大的訊息傳遞模式。

這篇文章,我們聊聊 Redis Stream 基本用法 ,以及如何在 SpringBoot 專案中應用 Redis Stream 。

1 基礎知識

Redis Stream 的結構如下圖所示,它是一個訊息連結串列,將所有加入的訊息都串起來,每個訊息都有一個唯一的 ID 和對應的內容。

每個 Redis Stream 都有唯一的名稱 ,對應唯一的 Redis Key 。

同一個 Stream 可以掛載多個消費組 ConsumerGroup , 消費組不能自動建立,需要使用 XGROUP CREATE 命令建立

每個消費組會有個遊標 last_delivered_id,任意一個消費者讀取了訊息都會使遊標 last_delivered_id 往前移動 ,標識當前消費組消費到哪條訊息了。

消費組 ConsumerGroup 同樣可以掛載多個消費者 Consumer , 每個 Consumer 並行的讀取訊息,任意一個消費者讀取了訊息都會使遊標 last_delivered_id 往前移動。

消費者內部有一個屬性 pending_ids , 記錄了當前消費者讀取但沒有回覆 ACK 的訊息 ID 列表 。

2 核心命令

01 XADD 向 Stream 末尾新增訊息

使用 XADD 向佇列新增訊息,如果指定的佇列不存在,則建立一個佇列。基礎語法格式:

XADD key ID field value [field value ...]
  • key :佇列名稱,如果不存在就建立
  • ID :訊息 id,我們使用 * 表示由 redis 生成,可以自定義,但是要自己保證遞增性。
  • field value : 記錄。
127.0.0.1:6379> XADD mystream * name1 value1 name2 value2
"1712473185388-0"
127.0.0.1:6379> XLEN mystream
(integer) 1
127.0.0.1:6379> XADD mystream * name2 value2 name3 value3
"1712473231761-0"

訊息 ID 使用 * 表示由 redis 生成,同時也可以自定義,但是自定義時要保證遞增性。

訊息 ID 的格式: 毫秒級時間戳 + 序號 , 例如:1712473185388-5 , 它表示當前訊息在毫秒時間戳 1712473185388 產生 ,並且該毫秒內產生到了第5條訊息。

在新增佇列訊息時,也可以指定佇列的長度

127.0.0.1:6379> XADD mystream MAXLEN 100 * name value1 age 30
"1713082205042-0"

使用 XADD 命令向 mystream 的 stream 中新增了一條訊息,並且指定了最大長度為 100。訊息的 ID 由 Redis 自動生成,訊息包含兩個欄位 nameage,分別對應的值是 value130

02 XRANGE 獲取訊息列表

使用 XRANGE 獲取訊息列表,會自動過濾已經刪除的訊息。語法格式:

XRANGE key start end [COUNT count]
  • key :佇列名
  • start :開始值, - 表示最小值
  • end :結束值, + 表示最大值
  • count :數量
127.0.0.1:6379> XRANGE mystream - + COUNT 2
1) 1) "1712473185388-0"
   2) 1) "name1"
      2) "value1"
      3) "name2"
      4) "value2"
2) 1) "1712473231761-0"
   2) 1) "name2"
      2) "value2"
      3) "name3"
      4) "value3"

我們得到兩條訊息,第一層是訊息 ID ,第二層是訊息內容 ,訊息內容是 Hash 資料結構 。

03 XREAD 以阻塞/非阻塞方式獲取訊息列表

使用 XREAD 以阻塞或非阻塞方式獲取訊息列表 ,語法格式:

XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] id [id ...]
  • count :數量
  • milliseconds :可選,阻塞毫秒數,沒有設定就是非阻塞模式
  • key :佇列名
  • id :訊息 ID
127.0.0.1:6379> XREAD streams mystream 0-0
1) 1) "mystream"
   2) 1) 1) "1712473185388-0"
         2) 1) "name1"
            2) "value1"
            3) "name2"
            4) "value2"
      2) 1) "1712473231761-0"
         2) 1) "name2"
            2) "value2"
            3) "name3"
            4) "value3"

XRED 讀訊息時分為阻塞非阻塞模式,使用 BLOCK 選項可以表示阻塞模式,需要設定阻塞時長。非阻塞模式下,讀取完畢(即使沒有任何訊息)立即返回,而在阻塞模式下,若讀取不到內容,則阻塞等待。

127.0.0.1:6379> XREAD block 1000 streams mystream $
(nil)
(1.07s)

使用 Block 模式,配合 $ 作為 ID ,表示讀取最新的訊息,若沒有訊息,命令阻塞!等待過程中,其他客戶端向佇列追加訊息,則會立即讀取到。

因此,典型的佇列就是 XADD 配合 XREAD Block 完成。XADD 負責生成訊息,XREAD 負責消費訊息。

04 XGROUP CREATE 建立消費者組

使用 XGROUP CREATE 建立消費者組,分兩種情況:

  • 從頭開始消費:
XGROUP CREATE mystream consumer-group-name 0-0  
  • 從尾部開始消費:
XGROUP CREATE mystream consumer-group-name $

執行效果如下:

127.0.0.1:6379> XGROUP CREATE mystream mygroup 0-0
OK

05 XREADGROUP GROUP 讀取消費組中的訊息

使用 XREADGROUP GROUP 讀取消費組中的訊息,語法格式:

XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS key [key ...] ID [ID ...]
  • group :消費組名
  • consumer :消費者名。
  • count : 讀取數量。
  • milliseconds : 阻塞毫秒數。
  • key : 佇列名。
  • ID : 訊息 ID。

示例:

127.0.0.1:6379>  XREADGROUP group mygroup consumerA count 1 streams mystream >
1) 1) "mystream"
   2) 1) 1) "1712473185388-0"
         2) 1) "name1"
            2) "value1"
            3) "name2"
            4) "value2"

消費者組 mygroup 中的消費者 consumerA ,從 名為 mystream 的 Stream 中讀取訊息。

  • COUNT 1 表示一次最多讀取一條訊息
  • > 表示訊息的起始位置是當前可用訊息的 ID,即從當前未讀取的最早訊息開始讀取。

06 XACK 訊息消費確認

接收到訊息之後,我們要手動確認一下(ack),語法格式:

xack key group-key ID [ID ...]

示例:

127.0.0.1:6379> XACK mystream mygroup 1713089061658-0
(integer) 1

消費確認增加了訊息的可靠性,一般在業務處理完成之後,需要執行 ack 確認訊息已經被消費完成,整個流程的執行如下圖所示:

我們可以使用 xpending 命令檢視消費者未確認的訊息ID

127.0.0.1:6379> xpending mystream mygroup
1) (integer) 1
2) "1713091227595-0"
3) "1713091227595-0"
4) 1) 1) "consumerA"
      2) "1"

07 XTRIM 限制 Stream 長度

我們使用 XTRIM 對流進行修剪,限制長度, 語法格式:

127.0.0.1:6379> XADD mystream * field1 A field2 B field3 C field4 D
"1712535017402-0"
127.0.0.1:6379> XTRIM mystream MAXLEN 2
(integer) 4
127.0.0.1:6379> XRANGE mystream - +
1) 1) "1712498239430-0"
   2) 1) "name"
      2) "zhangyogn"
2) 1) "1712535017402-0"
   2) 1) "field1"
      2) "A"
      3) "field2"
      4) "B"
      5) "field3"
      6) "C"
      7) "field4"
      8) "D"

3 SpringBoot Redis Stream 實戰

1、新增 SpringBoot Redis 依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2、yaml 檔案配置

3、RedisTemplate 配置

4、定義stream監聽器

5、定義streamcontainer 並啟動

6、傳送訊息

執行完成之後,消費者就可以列印如下日誌:

演示程式碼地址:

https://github.com/makemyownlife/courage-cache-demo

4 Redis stream 用做訊息佇列完美嗎

筆者認為 Redis stream 用於訊息佇列最大的進步在於:實現了釋出訂閱模型

釋出訂閱模型具有如下特點:

  • 消費獨立

    相比佇列模型的匿名消費方式,釋出訂閱模型中消費方都會具備的身份,一般叫做訂閱組(訂閱關係),不同訂閱組之間相互獨立不會相互影響。

  • 一對多通訊

    基於獨立身份的設計,同一個主題內的訊息可以被多個訂閱組處理,每個訂閱組都可以拿到全量訊息。因此釋出訂閱模型可以實現一對多通訊。

細品 Redis stream 的設計,我們發現它和 Kafka 非常相似,比如說消費者組,消費進度偏移量等。

我們曾經詬病 Redis List 資料結構用做佇列時,因為消費時沒有 Ack 機制,應用異常掛掉導致訊息偶發丟失的情況,Redis Stream 已經完美的解決了。

因為消費者內部有一個屬性 pending_ids , 記錄了當前消費者讀取但沒有回覆 ACK 的訊息 ID 列表 。當消費者重新上線,這些訊息可以重新被消費。

但 Redis stream 用做訊息佇列完美嗎 ?

這個真沒有!

1、Redis 本身定位是記憶體資料庫,它的設計之初都是為快取準備的,並不具備訊息堆積的能力。而專業訊息佇列一個非常重要的功能是資料中轉樞紐,Redis 的定位很難滿足,所以使用起來要非常小心。

2、Redis 的高可用方案可能丟失訊息(AOF 持久化 和 主從複製都是非同步 ),而專業訊息佇列可以針對不同的場景選擇不同的高可用策略。

所以,筆者認為 Redis 非常適合輕量級訊息佇列解決方案,輕量級意味著:資料量可控 + 業務模型簡單 。


參考文章:

https://redis.io/docs/data-types/streams/

https://www.runoob.com/redis/redis-stream.html

https://pdai.tech/md/db/nosql-redis/db-redis-data-type-stream.html


筆者開源專案推薦:

簡單易用的簡訊服務: https://github.com/makemyownlife/platform-sms

分庫分表實戰演示:https://github.com/makemyownlife/shardingsphere-jdbc-demo

如果我的文章對你有所幫助,還請幫忙點贊、在看、轉發一下,你的支援會激勵我輸出更高質量的文章,非常感謝!

相關文章