Pub/Sub vs Stream:Redis 版的訊息系統

· tech

#redis#distributed-systems

📑 目錄

Redis 也能當訊息系統,但它有兩套截然不同的東西,用錯就會莫名其妙掉訊息、或殺雞用牛刀:Pub/Sub(廣播,丟了就丟)和 Stream(留著的 log,像一台縮小版 Kafka)。這是整個 Redis 系列的收尾,也剛好把它接回 Kafka——因為 Stream 幾乎就是把 Kafka 的核心概念,濃縮進一個 Redis 資料結構。

一個丟了就丟,一個留著可重播

Pub/Sub:廣播,丟了就丟 PUBLISH news "…" channel: news(不留存) Sub 線上✓ 收到 Sub 線上✓ 收到 Sub 離線✗ 漏掉 沒人在聽 → 沒了・無持久・無重播・無 ack Stream:留著的 log,可重播 XADD stream * …(append) m1m2m3m4m5 舊 consumer另一個從這讀 訊息留著(可設 MAXLEN 上限) 可從任意位置重讀・離線再上線能補・有 group + ack
Pub/Sub 是純廣播:訊息發到 channel,只有當下正在訂閱的人收得到,離線的、晚來的一律漏掉——沒有留存、沒有重播、沒有 ack(fire-and-forget)。Stream 則是一條 append-only 的 log:訊息 XADD 進去就留著(可設 MAXLEN 上限),consumer 能從任意位置讀、離線後上線還能從上次位置補讀,還帶 consumer group 與 ack。這個「丟了就丟 vs 留著記帳」的差別,跟 RabbitMQ 的 queue vs Kafka 的 log 是同一條軸

Pub/Sub:廣播,適合「漏了也沒差」的即時通知

Pub/Sub 的模型三句話講完:SUBSCRIBE 訂閱一個 channel、PUBLISH 往 channel 發、所有當下在線的訂閱者立刻收到。它是 fire-and-forget——broker 不記誰收過、不重送、不留存。所以它天生只適合「漏一兩則也無所謂」的即時廣播:線上狀態、即時通知、跨節點的快取失效廣播(叫大家把某個 key 清掉)。

SUBSCRIBE news          # 訂閱(這條連線進入訂閱模式)
PUBLISH news "hello"    # 另一條連線發佈,只有此刻在線的訂閱者收得到

千萬別拿它做「不能掉」的任務派發——訂閱者重啟的那幾秒、網路抖一下,那段時間的訊息就永遠消失了,而且你連「掉了」都不會知道。

Stream:留著的 log + consumer group,像輕量 Kafka

要「不能掉、可重播、多個 worker 分工」,就用 Stream。它是一條 append-only log,XADD 寫入、每則有一個遞增 ID;更關鍵的是它有消費者群組(consumer group),行為幾乎就是 Kafka 的翻版:

Stream + consumer group:分工、ack、沒 ack 就重送 stream m1m2m3m4m5m6 group: workers C1 C2 m1·m3·m5m2·m6 m4 給了 C2 但沒 XACK(C2 掛了)→ 留在 pending(PEL)→ 重新指派重送 處理完 XACK → 移出 pending 沒 ack 的一定被重送 → at-least-once
一個 consumer group(workers)裡的多個 consumer 分工消費同一條 stream——每則訊息只交給組內一個人(C1 拿 m1/m3/m5、C2 拿 m2/m6),這樣加 consumer 就能水平擴。處理完要 XACK 確認;沒 ack 的訊息(像 C2 拿了 m4 卻掛掉)會留在 pending(PEL),之後被重新指派、重送——這就是 at-least-once。看出來了嗎?這幾乎是 Kafka consumer group 的縮小版
XADD orders * item book qty 2                 # 寫一則(* = 自動生遞增 ID)
XGROUP CREATE orders workers 0                 # 建一個從頭開始的 group
XREADGROUP GROUP workers c1 COUNT 10 STREAMS orders >   # c1 取新訊息
XACK orders workers 1699-0                      # 處理完,確認這則(否則留在 PEL)
XPENDING orders workers                         # 看有哪些「拿了還沒 ack」的

那到底何時該直接上 Kafka?

Stream 這麼像 Kafka,那還要 Kafka 幹嘛?分界在規模與定位:

  • 用 Redis Stream:輕量的訊息 / 任務佇列,量沒有大到誇張、保留時間短、你本來就有 Redis、不想為了一個佇列再架一整套 Kafka。
  • 直接上 Kafka:超大 throughput、要長期保留(TB 級、幾天到幾週,可回溯重算)、巨量 fan-out(幾十個消費組各讀一份)、要生態(Kafka Connect、Streams)、要更硬的持久保證。Redis Stream 的資料活在記憶體裡(靠 RDB/AOF 落盤、又通常設 MAXLEN 截斷),它天生不是為「保留海量歷史」設計的。

一句話:Redis Stream 是「順手好用的輕量佇列」,不是「Kafka 的替代品」。 需求還小就別為了一個佇列扛一座 Kafka;但真需要 Kafka 那些能力時,硬用 Stream 撐,就是在重造一個殘缺的 Kafka。

反思

「丟了就丟」還是「留著記帳」,先問這一句

Pub/Sub 和 Stream 的選擇,說到底只有一個問題:漏掉一則,你承受得起嗎? 承受得起(即時通知、狀態廣播),Pub/Sub 又輕又簡單;承受不起(訂單、扣款、任務派發),就得用會留存、能重播、有 ack 的 Stream。這跟我在 RabbitMQ 那條「留著 vs 拿走」、Kafka 的「log vs queue」是同一種思考——選訊息方案前,先想清楚『漏一則會發生什麼』,答案自己就把工具挑好了。我看過太多事故,root cause 就是拿 fire-and-forget 的東西去送「絕對不能掉」的訊息,還一直以為是別的地方壞了。

好的抽象會「長成同一個形狀」

Redis Stream 讓我很有感的一點:它幾乎是把 Kafka 的核心——append log、offset、consumer group、ack、pending——濃縮進一個 Redis 資料結構。兩個團隊、兩套完全不同的實作,最後長出的骨架卻高度雷同。這說明那些概念不是 Kafka 的專利,而是「可靠的持久訊息」這個問題的自然解——誰認真去做,都會長出 log + offset + group + ack 這副形狀。這也是我學東西越來越省力的原因:看懂一個好抽象,就看懂了一票。學透 Kafka 的 consumer group,再看 Redis Stream、看別家的訊息系統,都是同一個故事換套皮。

收尾:Redis 的每一面,都在證明「它不只是快取」

寫到這篇,整個 Redis 系列剛好收口。回頭看這一路——資料結構、單執行緒、持久化、過期淘汰、快取三災、分散式鎖、複製、Sentinel、Cluster、交易與 Lua、到這篇的訊息系統——每一面其實都在證明開篇那句話:Redis 是一台記憶體資料結構伺服器,「快取」只是它最出名的一個用法。 它能當鎖、當佇列、當排行榜、當計數器、當輕量訊息 log,是因為它給了你一套又快又原子的資料結構,剩下的是你怎麼組合。這也是我最想留給讀完整個系列的你的一句話:別把 Redis 框在「快取」那個小盒子裡——當你開始把它當「手邊一台隨叫隨到的資料結構伺服器」,很多原本要架大東西才能解的問題,會突然變得又輕又簡單。