🎯 什麼情境該想到我
當你「要把一則訊息,送給一份動態算出的收件人通道清單」的時候。
⚙️ 怎麼用(步驟 / 公式)
意圖:把訊息路由給收件人清單上的所有通道。
做法要點:
- 先依規則(或訊息內容)計算出這則訊息該送給哪些收件人。
- 再把訊息複製、送往清單上每一個通道。
- 像內容路由器,但可以有多個目的地。
- 常用於審批、比價等「要同時問多個對象」的情境。
★★ 和發布訂閱的界線:這是事件驅動架構最容易失守的一條線
// ★ 收件人清單:發送者知道收件者是誰
recipients := computeRecipients(order) // ★ 發送者算出清單
for _, r := range recipients { publish(ctx, r, msg) }
// ★ 發布訂閱:發送者不知道也不在乎
publish(ctx, "orders.order.placed.v1", msg) // ★ 誰要收是訂閱者的事★★ 判準:加一個新的收件者,需要改發送者嗎?
需要 → 收件人清單。不需要 → 發布訂閱。★ 這兩者都是正當的模式,但常被搞混:
很多團隊以為自己在做事件驅動,實際上程式碼裡有一串if hasX { publish(…) }
——那是收件人清單,而且它的耦合是真實存在的(見 發布訂閱通道)。
★ 什麼時候收件人清單才是對的答案
| 用收件人清單 | 用發布訂閱 |
|---|---|
| ★ 清單依「訊息內容」而變(VIP 客戶多通知一個風控服務) | 收件者固定,或由訂閱者自己決定 |
| ★ 發送的是命令(必須有人做,而且要知道是誰) | 發送的是事件 |
| ★ 需要知道「送給了誰」(稽核、合規) | 不需要 |
| ★ 要收集每個收件者的回覆 → 升級成 散發-聚集 | 不需要回覆 |
★ 最典型的正當場景:比價/招標。
「這個詢價要送給哪三家供應商」是由訊息內容決定的動態清單,
發布訂閱表達不了「只送給這三家」。
★ Go 的實作與三個必須處理的失敗
func send(ctx context.Context, recipients []string, msg Message) error {
var errs []error
for _, r := range recipients {
if err := publish(ctx, r, msg); err != nil {
errs = append(errs, fmt.Errorf("%s: %w", r, err)) // ★ 不要第一個失敗就中斷
}
}
return errors.Join(errs...) // ★ Go 1.20+:把多個錯誤合起來
}★★ 這裡的三個設計問題,EIP 書裡談得很少,但每一個都會出事:
① ★★ 部分失敗
送給 5 個收件者,第 3 個失敗 → 怎麼辦?
★ 重送整則 → 前兩個會收到第二份(★ 所以收件者必須冪等)
★ 只重送失敗的 → ★ 需要記錄「誰已經成功了」⇒ 這個元件變成有狀態的
→ ★ 務實解:接受「重送全部」,把冪等做在收件者端([[冪等接收者]])
② ★ 清單為空
computeRecipients 回傳空清單 → ★ 訊息就這樣消失了,而且沒有錯誤
→ ★★ 一定要明確處理:是正常的(沒有符合的收件者)還是 bug?
曖昧就送 [[無效訊息通道]] 並計數
③ ★ 清單爆量
規則寫錯 → 一則訊息送給 10,000 個收件者 → ★ 下游被打爆
→ ★ 設上限並在超過時告警(不要靜默送出去)
★ 清單怎麼算出來(三種來源,代價不同)
| 來源 | 代價 |
|---|---|
訊息內容本身(msg.Recipients) | ★ 最簡單;但發送者要知道通道名 |
| 查資料庫/設定 | ★ 每則訊息一次查詢 → 要快取,而快取要失效 |
| 呼叫外部服務 | ★★ 同步依賴:那個服務掛了,路由就停了(見 內容擴充器 的同一問題) |
★ 兄弟辨析
| 差別 | |
|---|---|
| 發布訂閱通道 | ★ 發送者不知道收件者;★ 加一個下游不用改發送者 |
| 內容路由器 | 恰好一個目的地 |
| 分割器 | ★ 依內容項目拆成不同的訊息;收件人清單是複製同一則 |
| 散發-聚集 | ★ 收件人清單 + 聚合器(送出去之後還要收集回覆) |
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
-
需決定清單如何產生與維護。
-
送給多個收件者時,要考慮個別失敗的處理。
-
與 Publish-Subscribe 不同:清單是明確計算出、可控的一組收件者。
-
★★ 判準:加一個新收件者要改發送者嗎? 要 → 收件人清單;不要 → 發布訂閱。很多「事件驅動」其實是這個模式。
-
★★ 部分失敗沒有好答案。重送全部(收件者必須冪等)或記錄誰成功了(元件變有狀態)。務實選前者。
-
★★ 清單為空時訊息會靜默消失。一定要明確判斷是正常還是 bug,曖昧就送無效訊息通道並計數。
-
★ 清單要有上限。規則寫錯導致送給一萬個收件者會打爆下游。
-
★ 清單來源若是呼叫外部服務,就引入了同步依賴——那個服務掛了路由就停了。
-
★ 不要第一個收件者失敗就中斷。用
errors.Join收集所有錯誤。 -
收件者必須冪等,因為重送是唯一務實的失敗處理。
-
需要收集回覆時,這就升級成 散發-聚集,複雜度完全不同(有狀態、要逾時)。
🔗 相關工具
- 內容路由器 Content-Based Router(單一目的地 vs. 收件人清單的多目的地)
- 散發-聚集 Scatter-Gather(送給多方後再彙整回覆)
- 分割器 Splitter(也產生多則訊息,但依項目拆分)
- 企業整合模式
- 發布訂閱通道 —— ★ 最容易混淆的一組
- 內容路由器 —— 單一目的地
- 分割器 —— 拆分而非複製
- 散發-聚集 —— 加上收集回覆
- 冪等接收者 —— ★ 部分失敗的唯一務實解
- 無效訊息通道 —— 清單為空時的去處