🎯 什麼情境該想到我
當你「收到的訊息缺了下游需要的資料,得先從別的來源(資料庫、其他服務)查出來補齊,再往下送」的時候。
⚙️ 怎麼用(步驟 / 公式)
意圖:在訊息傳遞過程中,用外部資源提供的資料補足訊息中缺少的欄位,讓下游拿到完整資訊。
做法 / 結構要點:
- 攔截訊息,判斷缺哪些資料。
- 依訊息中已有的鍵(如客戶 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 // ★★ 一定要記,否則下游不知道這資料有多舊
}★ 重放時特別危險:你重放三個月前的訂單事件,
擴充器會補上「今天的」客戶等級——而當時他是別的等級。
→ ★ 需要歷史正確性的流程,不能用「查當前值」的擴充器,
必須是「事件當時就帶上」(回到方案 ①)。
★ 兄弟辨析
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
-
每則訊息都查外部來源,可能成為效能瓶頸與相依點。
-
外部來源不可用時要有失敗處理策略。
-
與內容過濾器相反:一個補資料、一個減資料。
-
★★ 擴充器把非同步訊息鏈接上同步依賴。對方掛了整條流停止。
-
★★ 先問「這資料可以是幾秒前的版本嗎」。可以 → 讓生產者帶上,或消費者自維讀模型。同步擴充是最後手段。
-
★★ 必須區分「查不到」(永久錯誤)與「查不了」(暫時錯誤)。搞反了會讓訊息無謂地重試到 DLQ,或讓大量訊息帶著預設值流到下游。
-
★★ 重放時擴充器會補上「今天的」值,而不是事件當時的值。需要歷史正確性就不能用擴充器。
-
★ 必須有超時,否則擴充會讓整個消費迴圈卡死。
-
★ 要有斷路器與批次查詢。逐筆查 100 次是常見的效能事故。
-
要記
EnrichedAt,否則下游不知道補上來的資料有多舊。 -
擴充器失敗時的預設值是業務決定。「查不到客戶就當普通等級」可能造成營收損失。
🔗 相關工具
- 內容過濾器 Content Filter(同家族的相反操作,移除欄位)
- 寄物單 Claim Check(同家族,改用取件單換取大內容)
- 企業整合模式
- 內容過濾器 —— 反向操作
- 訊息轉換器 —— 無 I/O 的純轉換
- 寄物單 —— 反向思路
- 事件訊息 —— ★ 事件攜帶狀態轉移=不需要擴充器
- 死信通道 —— 錯誤分類
- 工具-服務容錯設計 —— 超時、斷路器