🎯 什麼情境該想到我

當你「想通知其他系統『發生了某件事』,讓它們據此更新自己」的時候。

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

意圖:用事件訊息做可靠的非同步事件通知,達成系統間狀態同步。

做法要點:

  • 一有事件發生就立即發出訊息;內容通常精簡(表達「事件發生」這個事實)。
  • 時效性重要、資料量次要——重點是「盡快讓別人知道」。
  • 常配合 Publish-Subscribe Channel,讓多個訂閱者都收到通知;等同跨系統版的觀察者模式。

★★ 事件的四種用法(Fowler 的分類,實務上差別很大)

事件裡放什麼消費者怎麼用★ 代價
① 事件通知(thin event)只有 ID + 事件型別★ 回頭呼叫來源 拿詳情★ 又變成同步依賴;來源被打爆
② ★ 事件攜帶狀態轉移★ 完整的資料快照自己存一份讀模型,不必回頭問★ 訊息變大;資料會過時;schema 耦合更深
③ 事件溯源所有變更事件就是唯一真相重播事件重建狀態★★ 最重的:schema 永遠不能刪、重播要能重現
④ CQRS讀寫模型分開讀模型由事件更新★ 雙模型、最終一致性

★★ ② 是絕大多數團隊實際需要的那一個,也是最常被跳過的。
① 看起來「乾淨」(訊息小、不洩漏 schema),但它把非同步又變回同步——
五個消費者收到 OrderPlaced 之後同時回頭打 GET /orders/42,
來源服務的負載反而變成扇出的倍數。

★ ③④ 是完全不同量級的架構決策,不要因為「我們在用事件」就滑進去。

// ① 事件通知(thin)—— ★ 消費者必須回頭問
type OrderPlaced struct{ OrderID string }
 
// ② ★ 事件攜帶狀態轉移 —— 消費者自足
type OrderPlaced struct {
    OrderID    string    `json:"order_id"`
    CustomerID string    `json:"customer_id"`
    Lines      []Line    `json:"lines"`
    Total      Money     `json:"total"`
    OccurredAt time.Time `json:"occurred_at"`   // ★ 事件發生時間,不是送出時間
    Version    int64     `json:"version"`       // ★★ 讓消費者能丟棄過舊的事件
}

★ Version(或 OccurredAt)是讓事件處理與順序無關的關鍵:
消費端「只在 event.Version > stored.Version 時才更新」
→ 即使亂序、即使重複,結果都正確,而且可以完全並行(見 訊息序列)。

★★ 事件的三條紀律

① ★ 命名用過去式:OrderPlaced、PaymentCaptured、StockReserved
   ★ 過去式在提醒你一件事:它已經發生了,接收者不能拒絕
   → 所以事件裡不該有「請驗證」「請確認」的語義

② ★★ 發布者不可以知道訂閱者是誰
   判準:加一個新下游,需要改發布者嗎?需要 → 你在用 [[收件人清單]]
   (見 [[發布訂閱通道]])

③ ★★ 事件的 schema 是公共契約,而且訂閱者你可能不認識
   → 只能做向後相容變更(加可選欄位)
   → 不相容就開 v2 通道並行,直到舊消費者下線(見 [[工具-資料編碼與演進]])

★ 事件粒度:這是最難的設計決定

★ 太細:OrderFieldChanged{field: "status", old: "a", new: "b"}
   → 消費者要自己拼出業務含意;而且洩漏了你的資料模型(見 [[通道配接器]] 的 CDC 問題)

★ 太粗:SomethingHappened{payload: 整個聚合的所有欄位}
   → 消費者必須 diff 才知道發生了什麼

✅ 對的粒度:★ 以「業務上有名字的事情」為單位
   OrderPlaced / OrderCanceled / PaymentCaptured / ShipmentDispatched
   → ★ 判準:這個事件名能不能對業務人員講得通?

→ 這正是 領域事件 的定義。

★ 事件不是「免費的解耦」——三個真實代價

① ★★ 因果鏈變得看不見
   「這筆訂單為什麼沒出貨」→ 要追過五個通道
   → ★ correlation id 必須貫穿(見 [[關聯識別碼]]、[[訊息歷史]])

② ★ 最終一致性會浮現到 UI
   下單成功了,但「我的訂單」頁面還沒有它
   → ★★ 這是產品問題不是技術問題,要在設計階段就決定怎麼呈現

③ ★ 沒有人保證事件被處理了
   訂閱者掛了三天 → 沒有任何錯誤,只有積壓
   → ★ 每個訂閱者的 lag 都要監控告警

🧪 我實際套用的紀錄

  • (待填)

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

  • 重時效、可容忍精簡;若接收端需要完整資料,改用文件訊息或在事件內帶資料引用。

  • 發布訂閱下要考量多訂閱者與可靠傳遞。

  • 若目的是「請對方執行動作」而非「告知事實」,改用命令訊息。

  • ★★ 「事件通知」(只帶 ID)會把非同步變回同步,而且來源承受扇出倍數的查詢。多數團隊真正需要的是「事件攜帶狀態轉移」。

  • ★ 事件溯源與 CQRS 是完全不同量級的決策,不要因為「在用事件」就滑進去。

  • ★★ 事件帶 Version/OccurredAt,消費端丟棄過舊的 —— 這讓處理與順序無關,可以完全並行。

  • ★ 命名用過去式。過去式在提醒你:接收者不能拒絕。

  • ★★ 加一個新下游要改發布者,就不是事件而是命令(見 收件人清單)。

  • ★ 事件 schema 是公共契約,訂閱者你可能不認識。 只能做向後相容變更。

  • ★ 粒度以「業務上有名字的事情」為單位。欄位級變更事件洩漏資料模型;整個聚合快照逼消費者自己 diff。

  • ★ 最終一致性會浮現到 UI,這是產品設計問題,要提前決定怎麼呈現。

  • ★ 沒有人保證事件被處理了。訂閱者掛了只會有積壓,不會有錯誤——每個訂閱者的 lag 都要告警。

🔗 相關工具