🎯 什麼情境該想到我
當你「希望接收端一看到訊息從哪個通道來,就知道它是什麼型別、該怎麼處理」的時候。
⚙️ 怎麼用(步驟 / 公式)
意圖:讓每個通道只傳送同一種資料型別的訊息,使接收端能確定該通道所有訊息的處理方式。
做法 / 結構要點:
- 一種資料型別配一個專屬通道(例如訂單走訂單通道、庫存變更走庫存通道)。
- 接收端不必檢查訊息內容來判斷型別,通道本身就代表型別。
- 發送端負責把不同型別的訊息送到對應的通道。
★★ 為什麼值得守:不然每個消費者都得自己當路由器
// ❌ 一個通道混著放三種訊息 —— 每個消費者都要先判斷型別
for msg := range ch {
switch msg.Type {
case "order.placed": …
case "order.canceled": … // ★ 我根本不在乎這個,但還是得收下、解析、判斷、丟棄
case "user.updated": … // ★ 而且上游加一種型別,我的 switch 就會走到 default
}
}★ 成本是三重的:白收的網路與反序列化、每個消費者一份重複的判斷邏輯、
以及「上游加一種型別」變成所有消費者都要留意的事。
★ 這個模式的實際落地:通道命名規範
★ 型別即通道名(領域.實體.事件.版本)
orders.order.placed.v1
orders.order.canceled.v1
users.user.updated.v1
★ 通道名就是 schema 的識別——消費者訂閱 orders.order.placed.v1,
就知道自己會拿到什麼,不必看 body 再判斷。
RabbitMQ 上很自然(topic exchange + routing key 通配):
routing key: orders.order.placed
binding: orders.order.* ← 想收全部訂單事件
binding: orders.order.placed ← 只收下單
Kafka 上要在「一個 topic 一種型別」與「一個 topic 一個實體」之間選:
① 一個 topic 一種事件型別 → ★ 純粹,但同一張訂單的事件分散在多個 topic ⇒ 順序沒了
② ★ 一個 topic 一個實體(orders.order.v1,內含 placed/canceled/shipped)
→ 同一個 orderID 的所有事件在同一個分區 ⇒ ★ 順序保證
→ 代價:消費者要自己 switch(但這次是合理的——它們本來就相關)
★★ 判準:這些事件之間有沒有順序關係?
有(同一張訂單的生命週期)→ 選 ②,順序比純粹重要。
沒有(不同實體的獨立事件)→ 選 ①。
★ 這是 Kafka 特有的取捨,EIP 書裡沒有——因為 JMS 沒有分區的概念。
★ 型別安全怎麼落實到 Go
// ★ 讓「通道」與「訊息型別」在型別系統裡綁在一起
type Topic[T any] struct{ name string }
var OrderPlaced = Topic[OrderPlacedV1]{name: "orders.order.placed.v1"}
func Publish[T any](ctx context.Context, p Producer, t Topic[T], msg T) error { … }
Publish(ctx, p, OrderPlaced, OrderPlacedV1{…}) // ★ 傳錯型別編譯不過
Publish(ctx, p, OrderPlaced, UserUpdatedV1{…}) // ★ 編譯錯誤⚠️ Go 的泛型不能宣告在方法上(func (p *Producer) Publish[T any] 編譯錯誤),
所以 Publish 必須是自由函式。
★ 什麼時候刻意混放
- ★ 同一個實體的事件序列(見上的選項②)——為了順序而混放是對的。
- 信封裡帶
type欄位的通用事件通道(event log/稽核流)——目的就是「全部都收」。 - 無效訊息通道/死信通道——★ 它們天生混著各種型別。
- 量很小、只有一個消費者、不會演進。
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
-
型別很多時,通道數量也會跟著膨脹,增加管理成本。
-
若確實需要用一個通道傳多種型別,需改以訊息標頭標示型別並在接收端分派。
-
★★ Kafka 上「一個 topic 一種型別」會犧牲同一實體的事件順序。有順序關係就該「一個 topic 一個實體」。
-
★ 混放的成本是三重的:白收的頻寬、每個消費者一份重複判斷、上游加型別影響所有人。
-
★ 通道名要帶版本,它就是 schema 的識別。
-
★ DLQ 與無效訊息通道天生混著各種型別,不適用這個模式。
-
Go 的泛型不能宣告在方法上,型別安全的 publish 必須是自由函式。
-
RabbitMQ 的 routing key 通配很方便,但 binding 拓撲要進版控,否則沒人知道訊息為什麼流到那裡。
-
不要為了「純粹」把一個實體的事件拆成十個 topic。順序通常比純粹重要。
🔗 相關工具
- 訊息通道 Message Channel(資料型別通道是通道的一種用法)
- 無效訊息通道 Invalid Message Channel(收到不符該型別/無法處理的訊息時移到這裡)
- 訊息路由器 Message Router(可依型別把訊息分流到各自的資料型別通道)
- 企業整合模式
- 訊息通道 —— 通道命名與治理
- 訊息路由器 —— 混放時被迫自己做的事
- 選擇性消費者 —— 另一種只收部分訊息的做法
- 格式指示器 —— schema 版本怎麼標
- 工具-資料分區 —— 分區鍵與順序保證