🎯 什麼情境該想到我

當你「收到的訊息缺了下游需要的資料,得先從別的來源(資料庫、其他服務)查出來補齊,再往下送」的時候。

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

意圖:在訊息傳遞過程中,用外部資源提供的資料補足訊息中缺少的欄位,讓下游拿到完整資訊。

做法 / 結構要點:

  • 攔截訊息,判斷缺哪些資料。
  • 依訊息中已有的鍵(如客戶 ID)去外部來源(資料庫、Web 服務、其他系統)查詢。
  • 把查到的資料併入訊息,再送往下一站。

★★ 這是訊息轉換家族裡唯一有 I/O 的,所以它會失敗、會慢

// ★ 擴充器的本質:訊息 + 外部查詢 → 更完整的訊息
func enrich(ctx context.Context, e OrderPlaced) (EnrichedOrder, error) {
    cust, err := customers.Get(ctx, e.CustomerID)    // ★★ 這一行是同步依賴
    if err != nil { return EnrichedOrder{}, fmt.Errorf("get customer: %w", err) }
    return EnrichedOrder{Order: e, CustomerTier: cust.Tier}, nil
}

★★ 代價很具體:你把一條非同步的訊息鏈,接上了一個同步依賴。
customers 服務掛了 → 整條訊息流停止(或全部進 DLQ)。
這正是 遠端程序呼叫 的「可用性相乘」在訊息世界的版本。

★★ 先問:能不能不要擴充?

① ★★ 讓生產者一開始就帶上(事件攜帶狀態轉移)
   OrderPlaced 直接帶 CustomerTier
   → ★ 零查詢、零同步依賴、零延遲
   → 代價:生產者要知道下游需要什麼;訊息變大
   → ★ 這是預設答案(見 [[事件訊息]] 的四種用法)

② ★ 讓消費者自己維護一份讀模型
   消費者訂閱 customers.* 事件,在本地維護 customer_tier 表
   → ★ 查詢變成本地查詢,沒有同步依賴
   → 代價:多一份資料要維護;有最終一致性的延遲

③ 快取
   → ★ 減少查詢但不消除依賴;而且快取失效是新問題

④ 真的擴充(同步查)
   → ★ 只在「資料變動極快、必須是最新值」時才值得

★★ 判準:這個補上來的資料,可以是「幾秒前的版本」嗎?
可以 → 用 ① 或 ②。不可以(匯率、庫存、風控分數)→ 才需要同步擴充。

★ 真的要擴充時:四個必做的防護

func enrich(ctx context.Context, e OrderPlaced) (EnrichedOrder, error) {
    // ① ★ 超時 —— 沒有超時的擴充會讓消費迴圈卡死
    ctx, cancel := context.WithTimeout(ctx, 500*time.Millisecond)
    defer cancel()
 
    cust, err := customers.Get(ctx, e.CustomerID)
    switch {
    case err == nil:
        return EnrichedOrder{Order: e, CustomerTier: cust.Tier}, nil
 
    // ② ★★ 區分「查不到」與「查不了」—— 這兩件事的處置完全不同
    case errors.Is(err, ErrNotFound):
        // ★ 資料真的不存在 → 永久錯誤,用預設值或送 [[無效訊息通道]]
        return EnrichedOrder{Order: e, CustomerTier: TierDefault}, nil
    default:
        // ★ 服務掛了 → 暫時錯誤,讓它重試(見 [[死信通道]] 的錯誤分類)
        return EnrichedOrder{}, fmt.Errorf("enrich: %w", err)
    }
}
// ③ ★ 斷路器:對方掛了就快速失敗,不要繼續堆積(見 [[工具-服務容錯設計]])
// ④ ★ 批次擴充:一次查 100 筆而不是 100 次查 1 筆

★★ ② 是最常被寫錯的:把「查不到」當成暫時錯誤 → 那則訊息會重試到進 DLQ,
而它永遠不會成功。反過來把「服務掛了」當永久錯誤 → 大量訊息帶著預設值流到下游。

★★ 一個致命的細節:擴充的資料是「什麼時候」的?

type EnrichedOrder struct {
    Order        OrderPlaced
    CustomerTier string
    EnrichedAt   time.Time    // ★★ 一定要記,否則下游不知道這資料有多舊
}

★ 重放時特別危險:你重放三個月前的訂單事件,
擴充器會補上「今天的」客戶等級——而當時他是別的等級。
→ ★ 需要歷史正確性的流程,不能用「查當前值」的擴充器,
必須是「事件當時就帶上」(回到方案 ①)。

★ 兄弟辨析

差別
內容過濾器★ 反向:移除欄位
訊息轉換器★ 純轉換,無 I/O、不會失敗
寄物單反向思路:不帶資料,只帶位址
事件訊息★ 「事件攜帶狀態轉移」= 不需要擴充器

🧪 我實際套用的紀錄

  • (待填)

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

  • 每則訊息都查外部來源,可能成為效能瓶頸與相依點。

  • 外部來源不可用時要有失敗處理策略。

  • 與內容過濾器相反:一個補資料、一個減資料。

  • ★★ 擴充器把非同步訊息鏈接上同步依賴。對方掛了整條流停止。

  • ★★ 先問「這資料可以是幾秒前的版本嗎」。可以 → 讓生產者帶上,或消費者自維讀模型。同步擴充是最後手段。

  • ★★ 必須區分「查不到」(永久錯誤)與「查不了」(暫時錯誤)。搞反了會讓訊息無謂地重試到 DLQ,或讓大量訊息帶著預設值流到下游。

  • ★★ 重放時擴充器會補上「今天的」值,而不是事件當時的值。需要歷史正確性就不能用擴充器。

  • ★ 必須有超時,否則擴充會讓整個消費迴圈卡死。

  • ★ 要有斷路器與批次查詢。逐筆查 100 次是常見的效能事故。

  • 要記 EnrichedAt,否則下游不知道補上來的資料有多舊。

  • 擴充器失敗時的預設值是業務決定。「查不到客戶就當普通等級」可能造成營收損失。

🔗 相關工具