🎯 什麼情境該想到我

當你「一則訊息裡含有多個項目,要拆成多則各自處理」的時候。

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

意圖:把含多個元素的複合訊息,拆成多則訊息。

做法要點:

  • 分割器讀入一則含清單的訊息(例如一張含多品項的訂單)。
  • 為每個子項目發出一則獨立訊息,讓後續能各別處理。
  • 常在子訊息標頭附上序列資訊,方便日後用聚合器把結果合回。

★ 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 當分區鍵,否則子訊息會亂序。

  • 項目之間有相依就不是分割,那是 流程管理器。

  • 需要整批原子性就不要拆,拆完極難做到。

🔗 相關工具