🎯 什麼情境該想到我

當你「要把一個請求發給多個接收者,再把各自的回覆彙整成一個回應」(如比價)的時候。

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

意圖:把訊息送給多個收件者,並把它們的回覆重新彙整成單一訊息。

做法要點:

  • 散發:用收件人清單(指定收件者)或 Publish-Subscribe(廣播)把請求送給多方。
  • 聚集:用聚合器依完成條件與策略,把各方回覆合併(例如挑最低報價)。

★ 兩種散發方式,決定了整個模式的耦合度

① ★ 用 [[收件人清單]] 散發(我知道要問誰)
   → 適合「詢價要問這三家」——清單由業務規則決定
   → ★ 知道「應該收到幾個回覆」⇒ 完成條件明確

② ★ 用 [[發布訂閱通道]] 散發(誰想回誰回)
   → 適合「開放競標」——不知道有幾家會回
   → ★★ 完成條件只能靠逾時或「收到 N 個就夠了」

★★ 這個選擇決定了完成條件的難度,而完成條件是聚合器最難的部分(見 聚合器)。
② 幾乎必然要靠逾時,因為你不知道總數。

★★ 最重要的設計決定:不要等所有人

// ★ 三種完成策略,按實用度排序
type CompletionPolicy struct {
    MinResponses int           // ★★ 「收到 3 個就夠了」—— 最實用
    Timeout      time.Duration // ★ 永遠要有
    RequireAll   bool          // ★ 慎用:一個慢的參與者拖垮整個流程
}

★★ RequireAll 是最常見的設計錯誤:
五個參與者,其中一個的 p99 是 30 秒 →
你的整個流程的 p99 就是 30 秒(而且那還是「它沒掛」的情況)。

★ 這就是「可用性相乘」在非同步世界的版本:
五個 99.9% 的參與者,全部都要 → 0.999⁵ = 99.5%(見 遠端程序呼叫)。
而「收到 3 個就夠」的可用性遠高於 99.9%。

★ 實務準則:能接受部分回覆就接受。 比價用三家報價中最好的,
而不是「非得五家都回」。

★ Go 的實作:行程內版本先考慮

// ★ 如果參與者是同步 API,行程內版本簡單得多 —— 不需要任何訊息基礎設施
func scatterGather(ctx context.Context, suppliers []Supplier, req Request) []Quote {
    ctx, cancel := context.WithTimeout(ctx, 3*time.Second)   // ★ 整體逾時
    defer cancel()
 
    results := make(chan Quote, len(suppliers))
    var wg sync.WaitGroup
    for _, s := range suppliers {
        wg.Add(1)
        go func(s Supplier) {                  // ★ Go 1.22+ 迴圈變數已是每輪新建
            defer wg.Done()
            q, err := s.Quote(ctx, req)        // ★ 慢的參與者被 ctx 砍掉
            if err != nil { return }           // ★ 失敗就不算,不中斷其他人
            results <- q
        }(s)
    }
    wg.Wait(); close(results)
 
    var out []Quote
    for q := range results { out = append(out, q) }
    return out                                  // ★ 可能是部分結果,呼叫端要能接受
}

★★ 判準:參與者能同步回應嗎?
能 → 用上面這段,不要用訊息。errgroup + context 已經給你逾時、取消、並行。
不能(要幾分鐘、參與者可能離線)→ 才需要訊息版的散發-聚集。

★ 訊息版的三個額外負擔

① ★★ 回覆的關聯:每個參與者的回覆要帶 correlation id 回來
   → ★ 而且要能分辨「這是誰的回覆」(參與者身分也要帶)
   → 惡意/錯誤的參與者可能回覆別人的 correlation id ⇒ ★ 要驗證它在清單內

② ★★ 遲到的回覆
   逾時後才到的回覆要怎麼辦?
   → ★ 丟棄(但要計數,否則你不知道逾時設太短)
   → ★★ 絕對不要「補上去重新聚合」—— 下游已經收到結果了

③ ★ 參與者自己掛了
   它永遠不回 → 只有逾時能發現
   → ★ 要統計「每個參與者的回覆率」,否則你不知道有人長期沒在回

★ 第③點常被忽略但很致命:一個參與者靜默失效三個月,
流程每次都靠逾時完成、每次都少一家報價,而沒有任何告警。

★ 兄弟辨析

差別
組合訊息處理器★ 各部分做不同的事;散發-聚集是各接收者做同樣的事
收件人清單★ 只散發,不收回覆
聚合器收集端就是它
請求-回覆★ 一對一版本
流程管理器更一般化:可多回合、可分支、可補償

🧪 我實際套用的紀錄

  • (待填)

⚠️ 注意 / 什麼時候不適用

  • 需決定散發方式,以及聚集的完成條件與逾時(不必等齊所有回覆)。

  • 各接收者可能不回或很慢,要有容錯。

  • 與組合訊息處理器相似,但這裡是「同一請求問多方」而非「拆項目」。

  • ★★ RequireAll 是最常見的設計錯誤。一個慢的參與者決定整個流程的 p99;五個 99.9% 全都要 = 99.5%。

  • ★★ 能接受部分回覆就接受(「收到 3 個就夠」)。可用性遠高於「非得全部」。

  • ★★ 參與者能同步回應 → 用 errgroup + context,不要用訊息基礎設施。

  • ★ 遲到的回覆要丟棄並計數(計數讓你知道逾時設太短),絕對不要補上去重新聚合。

  • ★ 要驗證回覆來自清單內的參與者,否則惡意/錯誤的回覆可以污染聚合結果。

  • ★★ 要統計每個參與者的回覆率。一個參與者靜默失效三個月、每次都靠逾時完成,是不會告警的。

  • 永遠要有整體逾時。完成條件靠「收齊」時,缺一個就永遠不完成。

  • 呼叫端必須能接受部分結果,這是契約的一部分。

🔗 相關工具