🎯 什麼情境該想到我
當你「一則訊息裡含有多個項目,要拆成多則各自處理」的時候。
⚙️ 怎麼用(步驟 / 公式)
意圖:把含多個元素的複合訊息,拆成多則訊息。
做法要點:
- 分割器讀入一則含清單的訊息(例如一張含多品項的訂單)。
- 為每個子項目發出一則獨立訊息,讓後續能各別處理。
- 常在子訊息標頭附上序列資訊,方便日後用聚合器把結果合回。
★ Go 的實作:關鍵是「每個子訊息都要能獨立存活」
func split(ctx context.Context, order Order) error {
for i, line := range order.Lines {
sub := LineItem{
// ★★ 四個必須帶的欄位
SequenceID: order.ID, // ★ 這一組的識別(=[[關聯識別碼]])
Position: i + 1, // ★ 第幾則
Total: len(order.Lines), // ★ 共幾則 → 讓 [[聚合器]] 知道完成條件
MessageID: fmt.Sprintf("%s-%d", order.ID, i+1), // ★★ 決定性 ID,不要用 uuid
SKU: line.SKU, Qty: line.Qty,
// ★ 子訊息需要的「父層脈絡」要複製進來,不要讓下游回頭問
CustomerID: order.CustomerID,
ShipTo: order.ShipTo,
}
if err := publish(ctx, "fulfilment.line", sub); err != nil { return err }
}
return nil
}★★ MessageID 必須是決定性的(由父 ID + position 算出),不可以用 uuid.New():
分割器重送時,子訊息的 ID 要和上次一樣,下游的冪等去重才擋得住。
★ 這是分割器最容易寫錯的一行——用 uuid 看起來沒問題,
但分割器一崩潰重送,下游就會收到一整組「新的」子訊息,去重完全失效。
★★ 分割器的原子性問題:沒有好答案,只有取捨
★ 分割器發到第 3 則時崩潰 → 前 2 則已經出去了
重送 → ① ② 重複、③④⑤ 第一次
→ ★ 下游必須冪等(決定性 MessageID 讓這件事可行)
★ 想要「全有全無」?三種做法:
① Kafka transactions —— ★ 只在「Kafka → Kafka」封閉迴圈內成立
② ★ Outbox:在同一個交易裡寫入所有子訊息,再由 relay 發送
→ ★★ 這是最務實的做法(見 [[工具-Transactional-Outbox-Pattern]])
③ 接受部分發送 + 下游冪等 → ★ 最簡單,多數情況足夠
★★ 五個實務陷阱
① ★★ 空集合
訂單沒有任何 line → 分割器什麼都不發
→ ★ 下游的 [[聚合器]] 永遠等不到,永遠不會完成
→ ★ 必須明確處理:發一則 Total=0 的「空序列」訊息,或走另一條路徑
② ★★ 扇出放大
一則訂單 × 平均 20 個 line = 20 倍訊息量
→ ★ 極端情況(一張訂單 5,000 個 line)會瞬間灌爆下游
→ ★ 要設單則訊息的拆分上限並告警
③ ★ 子訊息缺少父層脈絡
下游只收到 {SKU, Qty} → ★ 必須回頭查訂單 ⇒ 又變成同步依賴
→ ★ 把下游需要的父層欄位複製進子訊息(這就是 [[事件訊息]] 的
「事件攜帶狀態轉移」)
④ ★ 順序
子訊息分散到不同分區 → 亂序
→ ★ 需要順序就用 SequenceID 當分區鍵(見 [[工具-資料分區]])
⑤ ★ 大訊息拆完更大
父訊息 1 MB,複製父層脈絡到 20 個子訊息 → ★ 總量變成 5 MB+
→ 脈絡很大時用 [[寄物單]],子訊息只帶位址
★ 什麼時候不要拆
- ★ 下游本來就能處理整批——拆開只是增加訊息數與聚合的複雜度。
- ★ 需要整批原子性(全部成功或全部失敗)→ 拆開之後這件事極難做到。
- ★ 項目之間有相依(第二項的處理需要第一項的結果)→ 那是 流程管理器。
- 項目數很少(2–3 個)且處理很快。
★ 兄弟辨析
| 差別 | |
|---|---|
| 聚合器 | ★ 反向:把 N 則合回 1 則 |
| 組合訊息處理器 | ★ 分割器 + 聚合器的組合(拆開 → 各自處理 → 合回) |
| 收件人清單 | ★ 複製同一則訊息給多個目的地;分割器產生不同的訊息 |
| 訊息序列 | 分割器產生的就是一個訊息序列 |
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
-
拆出的子訊息若之後要合併,需保留關聯資訊(序列 / 關聯 ID)。
-
子訊息數量可能很大,要留意後續負載。
-
常與聚合器成對使用(拆開處理、再彙整)。
-
★★ 子訊息的 MessageID 必須是決定性的(父 ID + position),不可用 uuid——否則分割器重送時下游的去重完全失效。
-
★★ 空集合會讓下游的聚合器永遠等不到。必須明確發一則
Total=0或走另一條路徑。 -
★★ 分割器沒有原子性。務實解是 Outbox,或接受部分發送 + 下游冪等。
-
★ 扇出放大:一張 5,000 個項目的訂單會瞬間灌爆下游。要設拆分上限並告警。
-
★ 子訊息要帶父層脈絡,否則下游回頭查 = 又變成同步依賴。
-
★ 複製父層脈絡會讓總量暴增。脈絡大時用 寄物單。
-
★ 需要順序就用 SequenceID 當分區鍵,否則子訊息會亂序。
-
項目之間有相依就不是分割,那是 流程管理器。
-
需要整批原子性就不要拆,拆完極難做到。
🔗 相關工具
- 聚合器 Aggregator(把拆開後的結果重新彙整)
- 組合訊息處理器 Composed Message Processor(分割器+路由+聚合器的組合)
- 訊息序列 Message Sequence(子訊息常帶序列資訊以利重組)
- 企業整合模式
- 聚合器 —— 反向操作
- 組合訊息處理器 —— 分割 + 處理 + 聚合
- 訊息序列 —— 分割產生的序列
- 收件人清單 —— 複製 vs 拆分
- 寄物單 —— 脈絡太大時
- 工具-Transactional-Outbox-Pattern —— 分割的原子性
- 工具-資料分區 —— 子訊息的順序