🎯 什麼情境該想到我

當你「有一件事發生,想讓所有關心它的接收者各自收到一份通知」的時候(事件廣播,例如「訂單成立」要通知庫存、通知帳務、通知寄信)。

⚙️ 怎麼用(步驟 / 公式)

意圖:用一個會把每則訊息複製給所有訂閱者的通道來廣播訊息。

做法 / 結構要點:

  • 發送者把訊息發布到通道,通道把它的副本送給每一個訂閱者,各收各的一份。
  • 訂閱者可自由加入或退出,發送者不需知道有誰在訂閱。
  • 適合事件通知:一個來源、多方各自反應。

★ 各家 broker 的實作方式差異很大

broker機制★ 要注意
Kafka一個 topic + 多個 consumer group,各自維護 offset★ 每個 group 獨立進度,可以重放——這是 Kafka 最大的優勢
RabbitMQfanout/topic exchange → 每個訂閱者一個自己的 queue★ 訂閱者必須先存在,否則訊息沒有 queue 可進就消失了
SNS → SQSSNS topic fan out 到多個 SQS queue★ 每個 queue 有自己的 DLQ 與重試
NATS 核心純 pub/sub★ 完全不持久化,訂閱者不在線就丟了(要持久化用 JetStream)

★★ 最重要的差異:「離線的訂閱者」會怎樣

RabbitMQ fanout:★ 訊息路由到「當下存在的 binding」
                 → 訂閱者的 queue 還沒建立 → 訊息直接消失(而且沒有錯誤)
                 → ★ 所以要用 durable queue,並在服務啟動前就宣告好 binding

Kafka:         ★ 訊息寫進 log,保留 retention.ms 那麼久
                 → 新的 consumer group 可以從頭讀(auto.offset.reset=earliest)
                 → ★ 「先上線消費者」不是前提,這讓演進容易得多

NATS 核心:      ★ 不在線就沒了(fire and forget)

★ 這決定了「加一個新的下游服務」有多容易:
Kafka 上是「開一個新 group 從頭讀」;RabbitMQ 上你得先建 queue 和 binding,
而且之前的訊息拿不回來。→ 見 持久訂閱者。

★★ 事件通道的設計紀律:發布者不可以知道訂閱者是誰

// ❌ 這是把發布訂閱寫成了收件人清單
if hasFulfilment { publish(ctx, "fulfilment.inbound", e) }
if hasBilling    { publish(ctx, "billing.inbound", e) }
// ★ 發布者開始關心「誰要收」→ 加一個下游就要改發布者,解耦完全失效
 
// ✅ 發布者只發一次到事件通道,誰要收是訂閱者的事
publish(ctx, "orders.order.placed.v1", e)

★★ 判準:加一個新的下游服務,需要改發布者嗎?
需要 → 你其實在用 收件人清單,不是發布訂閱。
這是事件驅動架構最容易悄悄失守的一條線。

而這條線只有在發的是事件而不是命令時才守得住:

★ 事件(過去式,OrderPlaced)→ 零到多個接收者都合理 → 發布訂閱
★ 命令(祈使句,SendEmail)  → 恰好一個接收者 → ★ 發成 pub/sub = 兩個服務都寄信

★ 三個實務陷阱

① ★★ 每個訂閱者都需要自己的冪等與 DLQ
   一個訂閱者處理失敗,不該影響其他訂閱者
   → Kafka:每個 group 獨立 offset ✅
   → RabbitMQ:每個訂閱者一個 queue,各自的 DLX ✅
   → ★ 但「一個共用 queue 多個不同用途的 consumer」就會互相干擾

② ★ 扇出放大
   一則訊息 × 20 個訂閱者 = 20 倍的下游負載
   ★ 而且新增訂閱者時,發布者完全不知道流量變了 → 要監控扇出比

③ ★ 事件的 schema 是公共契約,而且訂閱者你可能不認識
   → 只能做向後相容變更;不相容就開 v2 通道並行(見 [[工具-資料編碼與演進]])

★ 兄弟辨析

差別
點對點通道一則訊息只有一個人拿到
收件人清單★ 發布者算出清單再逐一送 ⇒ 發布者知道收件者是誰
持久訂閱者發布訂閱 + 「離線期間的訊息也要留著」
訊息匯流排整體架構層級:★ 一組共用的通道 + 標準格式,不是單一通道
觀察者模式★ 行程內的同一個想法(同步、無持久化、共享記憶體)

🧪 我實際套用的紀錄

  • (待填)

⚠️ 注意 / 什麼時候不適用

  • 若一則訊息只該被處理一次(命令/工作分派),用點對點通道,別廣播。

  • 訂閱者若在訊息發布時未上線,是否還收得到取決於是否為持久訂閱,需明確設定。

  • ★★ RabbitMQ 的 fanout 只送給「當下存在的 binding」。訂閱者還沒建立 queue → 訊息靜默消失。

  • ★★ 加一個新下游需要改發布者,就不是發布訂閱,而是 收件人清單。這條線最容易悄悄失守。

  • ★ 只有「事件」適合發布訂閱。把命令發成 pub/sub → 兩個服務都執行。

  • ★ 每個訂閱者要有自己的 offset/queue、自己的冪等、自己的 DLQ。共用一個 queue 會互相干擾。

  • ★ 扇出會放大負載:20 個訂閱者 = 20 倍下游流量,而發布者不知道。要監控扇出比。

  • ★ NATS 核心 pub/sub 完全不持久化。要持久化必須用 JetStream。

  • 事件 schema 是公共契約,訂閱者你可能不認識。只能做向後相容變更。

  • Kafka 的 retention 到了就真的沒了。重放能力有時間上限。

🔗 相關工具