🎯 什麼情境該想到我

當你「非同步的請求與回覆需要一個 ID 把它們配對起來」的時候。

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

意圖:讓請求者能把收到的回覆,對應回自己送出的那一筆請求。

做法要點:

  • 每個請求帶一個唯一的請求 ID。
  • 回覆訊息在標頭放一個 Correlation Identifier,其值等於原始請求的 ID。
  • 請求者收到回覆時,用這個關聯識別碼比對,找出對應的原始請求與其狀態。

★★ 三種 ID,三個不同用途,經常被混成一個

ID用途誰產生★ 生命週期
message-id★ 冪等去重(見 冪等接收者)生產者,每則訊息都不同一則訊息
correlation-id★ 串起一次業務流程(請求↔回覆、一次下單的所有事件)流程的起點,整條鏈共用同一個整個業務流程
trace-id(W3C traceparent)★ 可觀測性:在 APM 裡看到完整 span 樹入口(gateway/handler)一次分散式追蹤

★★ 最常見的錯誤是把 message-id 當 correlation-id 用:
重送同一則訊息時 message-id 相同(對),但衍生的下游訊息會各有自己的 id——
於是你無法把「一次下單產生的十則訊息」串起來。

★ 規則:correlation-id 只在流程起點產生一次,之後所有衍生訊息都原封不動地複製它。

type Envelope struct {
    MessageID     string `json:"message_id"`      // ★ 每則都不同(uuid)
    CorrelationID string `json:"correlation_id"`  // ★★ 整條鏈相同
    CausationID   string `json:"causation_id"`    // ★ 「我是被哪一則訊息引起的」
    TraceParent   string `json:"traceparent"`     // ★ W3C 格式,給 APM 用
}

★ causation-id 常被忽略,但它是唯一能重建「因果樹」的欄位:
correlation-id 告訴你「這些訊息屬於同一次流程」,
causation-id 告訴你「A 引起了 B,B 引起了 C」——出事時這才是你要的。

下單 API ──▶ OrderPlaced        corr=X  msg=1  cause=(none)
             ├──▶ ReserveStock  corr=X  msg=2  cause=1
             │     └──▶ StockReserved  corr=X  msg=4  cause=2
             └──▶ ChargeCard    corr=X  msg=3  cause=1
                   └──▶ ChargeFailed   corr=X  msg=5  cause=3
★ 用 corr=X 撈出全部;用 cause 鏈重建這棵樹 → 一眼看出是 ChargeCard 那條斷了

★★ 最重要的一件事:這件事必須在「第一天」就做

★ 事後補 correlation id 極貴:
  ① 每個服務的每個 publish 都要改(★ 而且漏一個就斷鏈)
  ② 已經在通道裡的歷史訊息沒有這個欄位,混合期間查詢邏輯要處理兩種
  ③ 日誌裡沒有 → 過去的事故永遠查不出來

★ 對策:把它做進共用的 client library,讓開發者「不必想起」它
(見 訊息匯流排):

// ★ 共用 library 自動傳遞 —— 開發者只要用 ctx,不必碰欄位
func Publish(ctx context.Context, topic string, msg any) error {
    e := Envelope{
        MessageID:     uuid.NewString(),
        CorrelationID: CorrelationFromContext(ctx),   // ★ 沒有就產生一個
        CausationID:   CausationFromContext(ctx),     // ★ 當前正在處理的 message id
        TraceParent:   traceParentFrom(ctx),
    }
    …
}
 
// ★ 消費端:把收到的訊息 id 放進 ctx,作為下游的 causation
func withMessageContext(ctx context.Context, e Envelope) context.Context {
    ctx = WithCorrelation(ctx, e.CorrelationID)
    ctx = WithCausation(ctx, e.MessageID)       // ★★ 關鍵的一行
    return ctx
}

★ 和日誌/追蹤打通

// ★ 每一行日誌都帶 correlation id —— 這是它最大的價值
logger := slog.With("correlation_id", corr, "message_id", e.MessageID)

★ W3C traceparent 是跨系統追蹤的標準格式,OpenTelemetry 直接認得。
不要自己發明格式——用 traceparent 就能讓訊息鏈出現在 APM 的 span 樹裡
(見 工具-遙測與監控)。

⚠️ 但 trace-id 不能取代 correlation-id:
trace 通常有取樣(只留 1%)和保留期(幾天);
業務流程的關聯需要 100% 保留、而且要能查幾個月前的。→ 兩者都要。

★ 兄弟辨析

差別
請求-回覆、回覆位址★ 這個 ID 是「對應哪個請求」那一半
訊息歷史★ 記錄「經過了哪些元件」;關聯識別碼只是一個 ID
冪等接收者★ 用的是 message-id,不是 correlation-id
聚合器★ 用 correlation id 判斷「哪些訊息屬於同一組」

🧪 我實際套用的紀錄

  • (待填)

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

  • ID 必須夠唯一,避免配錯。

  • 請求者需保存「待回覆請求」的狀態以供比對。

  • 常與回覆位址一起,構成完整的非同步問答。

  • ★★ message-id(去重)、correlation-id(串流程)、trace-id(可觀測性)是三件事,不要混成一個。

  • ★★ correlation-id 只在流程起點產生一次,之後所有衍生訊息原封不動複製。

  • ★ causation-id 才能重建因果樹。correlation-id 只告訴你「屬於同一次流程」。

  • ★★ 這件事必須第一天就做。事後補要改每個 publish,漏一個就斷鏈,而且過去的事故永遠查不出來。

  • ★ 做進共用 client library,讓開發者不必想起它。

  • ★ trace-id 不能取代 correlation-id:trace 有取樣(1%)和短保留期,業務關聯需要 100% 且長期保留。

  • 用 W3C traceparent 標準格式,不要自己發明——OpenTelemetry 直接認得。

  • 每一行日誌都要帶 correlation id,否則它只存在於訊息裡,查事故時用不到。

🔗 相關工具