🎯 什麼情境該想到我
當你「想通知其他系統『發生了某件事』,讓它們據此更新自己」的時候。
⚙️ 怎麼用(步驟 / 公式)
意圖:用事件訊息做可靠的非同步事件通知,達成系統間狀態同步。
做法要點:
- 一有事件發生就立即發出訊息;內容通常精簡(表達「事件發生」這個事實)。
- 時效性重要、資料量次要——重點是「盡快讓別人知道」。
- 常配合 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 都要告警。
🔗 相關工具
- 命令訊息 Command Message(請對方做事,而非告知事實)
- 文件訊息 Document Message(重資料內容,而非時效通知)
- 企業整合模式
- 命令訊息、文件訊息 —— 另外兩種語義
- 發布訂閱通道 —— 事件的正確通道
- 訊息序列 —— 用版本號讓處理與順序無關
- 關聯識別碼、訊息歷史 —— 追回因果鏈
- 領域事件 —— 事件粒度的定義
- 通道配接器 —— CDC 事件 vs 業務事件
- 工具-資料編碼與演進 —— 事件 schema 的相容性