🎯 什麼情境該想到我
當你「有一件事發生,想讓所有關心它的接收者各自收到一份通知」的時候(事件廣播,例如「訂單成立」要通知庫存、通知帳務、通知寄信)。
⚙️ 怎麼用(步驟 / 公式)
意圖:用一個會把每則訊息複製給所有訂閱者的通道來廣播訊息。
做法 / 結構要點:
- 發送者把訊息發布到通道,通道把它的副本送給每一個訂閱者,各收各的一份。
- 訂閱者可自由加入或退出,發送者不需知道有誰在訂閱。
- 適合事件通知:一個來源、多方各自反應。
★ 各家 broker 的實作方式差異很大
| broker | 機制 | ★ 要注意 |
|---|---|---|
| Kafka | 一個 topic + 多個 consumer group,各自維護 offset | ★ 每個 group 獨立進度,可以重放——這是 Kafka 最大的優勢 |
| RabbitMQ | fanout/topic exchange → 每個訂閱者一個自己的 queue | ★ 訂閱者必須先存在,否則訊息沒有 queue 可進就消失了 |
| SNS → SQS | SNS 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 到了就真的沒了。重放能力有時間上限。
🔗 相關工具
- 點對點通道 Point-to-Point Channel(對照:一則訊息只給一個接收者)
- 訊息通道 Message Channel(發布訂閱通道是通道的一種)
- 訊息匯流排 Message Bus(常以發布訂閱作為企業級事件廣播骨幹)
- 企業整合模式
- 點對點通道 —— 對照:只有一個人拿到
- 持久訂閱者 —— 離線期間不丟訊息
- 收件人清單 —— 發布者決定收件者(★ 不是發布訂閱)
- 事件訊息 —— 只有事件適合廣播
- 訊息匯流排 —— 架構層級的共用通道
- 觀察者模式 —— 行程內的同一個想法
- 工具-資料編碼與演進 —— 事件 schema 的相容性