Section 1:傳統資料庫的問題
你有沒有遇過這種情境:用戶打電話來說「我沒有取消訂單!」,但你打開資料庫一看,status = CANCELLED。 你知道這個狀態是錯的,但你完全不知道它是什麼時候、被誰、基於什麼原因改掉的。 傳統 CRUD 架構的核心問題就是:它只儲存「此刻的狀態」,歷史資訊永遠消失。
傳統 CRUD 的痛點(以電商訂單為例)
這些問題在一般的 CRUD 應用裡可能還撐得住。但在金融系統、電商平台、醫療記錄等 「每一個操作都必須可追溯」的場景中,傳統架構就會讓你付出慘痛代價—— 不是被法規罰款,就是面對一個你完全無法解釋的「資料為什麼變成這樣」的謎題。
Event Sourcing 的核心思想
Event Sourcing 的核心哲學是:不存「狀態」,只存「發生過的事情」。 每一個對系統的操作都會產生一個不可變的事件,這些事件永遠不會被修改或刪除,只能被追加。 而系統的「當前狀態」,是把所有歷史事件按順序重播(Replay)之後得到的結果。
完整審計日誌
每一個操作都有記錄,不可篡改,天然符合法規要求
任意重播歷史
可以重播到任意時間點的狀態,時光機般的除錯能力
多種讀取視圖
同一份事件,可以建立多個不同用途的查詢視圖
Section 2:Event Sourcing 基礎實作
理解了概念之後,來看實際的 TypeScript 實作。我們以電商訂單系統為例, 建立 Domain Events 的型別定義,以及只支援「追加」操作的 Event Store。 這是 Event Sourcing 架構的兩個最基礎的積木。
樂觀鎖(Optimistic Concurrency Control)是什麼?
當兩個使用者同時對同一個訂單發送操作時,可能導致「後一個操作覆蓋前一個」的競態條件。 樂觀鎖的做法是:在發送操作時帶上「我目前知道的版本號」, 如果 Event Store 裡的版本和預期不符,就代表有人在你之前修改了,操作拒絕,讓前端重試。 這比悲觀鎖(直接鎖定記錄)更適合高並發場景。
Section 3:Aggregate 與狀態重建
Aggregate 是 Domain-Driven Design(DDD)的概念,代表一組具有業務邊界的物件集合, 由一個「根(Aggregate Root)」統一管理對外的操作。 在 Event Sourcing 中,Aggregate 的職責有兩個:產生事件(接收 Command 時) 和重建狀態(從 Event Store 讀取事件時)。
為什麼要分開 raise 和 apply?raise() 負責把事件加入「待提交清單」,然後呼叫 apply()。apply() 只負責更新 in-memory 狀態,不管事件是「新產生的」還是「從 Event Store 重播的」。 這個設計讓 fromEvents() 可以直接呼叫 apply() 重建狀態, 不會產生多餘的「未提交事件」,避免重複儲存。
Section 4:CQRS — Command 與 Query 分離
CQRS(Command Query Responsibility Segregation,命令查詢職責分離)是一個設計模式, 但和 Event Sourcing 幾乎是天生一對。它的核心思想是:寫入的資料模型和讀取的資料模型應該分開設計, 因為這兩件事的需求完全不同。
實際上,CQRS 讓你可以針對讀取和寫入分別優化。寫入端(Event Store)只需要 支援高效的「追加事件」操作;讀取端(Projection)可以用任何最適合查詢的資料結構, 例如 PostgreSQL 做列表查詢、Elasticsearch 做全文搜尋、Redis 做即時計數。
Projection 的本質是「預先計算」:當 Event 發生時,立刻更新對應的 Read Model,讓查詢時可以直接讀取已經整理好的資料, 不需要在查詢時才去計算。這和 SQL 的 Materialized View 概念相似, 但 Projection 可以更靈活,跨越多個 Aggregate,也可以針對特定的查詢場景量身定做。
Section 5:Projection 重建(Rebuild)
Event Sourcing 最讓人驚豔的特性,就是你可以在任何時候「從頭重建任何 Projection」。 因為所有的事件都永遠保存在 Event Store 裡,只要你有新的查詢需求, 只需要建立新的 Projection,然後把所有歷史事件重播一遍—— 不需要寫任何資料遷移腳本,不需要修改既有的資料結構。
Projection 重建的實際應用場景:
(1) 你發現 Projection 有 bug,計算邏輯錯誤 → 修正邏輯後重建,歷史資料自動修正。
(2) 業務需要新的報表維度 → 建立新 Projection 重播即可,不需要 Data Migration。
(3) 要換查詢資料庫(PostgreSQL → Elasticsearch)→ 把 Projection 的儲存層換掉,重播一遍。
Section 6:Event Sourcing 的實際挑戰
Event Sourcing 不是萬能藥。它解決了傳統 CRUD 的審計和歷史問題, 但也引入了新的複雜性。在決定採用之前,必須清楚理解這些取捨。
Eventual Consistency(最終一致性)
CQRS 架構中,Command 成功(事件已寫入 Event Store)和 Projection 更新之間, 存在一段時間差。這段時間裡,用戶可能看到「過時的」讀取結果。 這就是「最終一致性」——資料最終會一致,但不是立即的。
Event Schema 演化
事件一旦寫入就永遠保存,這意味著你的程式碼必須能夠處理「舊版本的事件格式」。 當業務需求改變,需要修改事件的 data 結構時,必須謹慎處理向後相容。
何時適合 Event Sourcing?
適合採用的場景
- 需要完整審計日誌(金融、醫療、法律)
- 業務邏輯複雜、狀態機轉換多
- 需要重播歷史資料分析
- 微服務間需要可靠的事件通知
- 需要支援 Undo / Redo 功能
- 多個下游系統需要消費同一份資料
不建議採用的場景
- 簡單的 CRUD 應用(過度設計)
- 小團隊、快速原型(學習成本高)
- 查詢需求單一、不需要多種 Projection
- 團隊對 DDD / Event Sourcing 不熟悉
- 需要大量複雜的跨 Aggregate 查詢
- 強一致性要求(無法接受最終一致性)
Section 7:實際工具與框架
了解了理論之後,在生產環境中你不會自己實作 Event Store。 以下介紹幾個成熟的工具,以及如何和 Node.js / TypeScript 整合。
EventStoreDB — 專為 Event Sourcing 設計的資料庫
EventStoreDB 是業界最成熟的 Event Sourcing 專用資料庫, 提供原生的 Stream(即 Aggregate 的事件串列)、 樂觀鎖、Catch-up Subscription(即時推送新事件)等功能。
Snapshot 優化(解決 Replay 效能問題)
當一個 Aggregate 累積了幾千個事件時,每次重建狀態都要重播所有事件,效能會變差。 解法是「Snapshot」:定期把當前狀態存下來,重建時從最近的 Snapshot 開始, 只需要重播 Snapshot 之後的事件。
EventStoreDB
- • 專為 Event Sourcing 設計
- • 原生 Stream + 樂觀鎖支援
- • Catch-up Subscription
- • 適合:需要成熟 ES 方案
PostgreSQL(自建)
- • 用 events 表 + 樂觀鎖
- • 搭配 LISTEN/NOTIFY 推送
- • 或用 Debezium CDC 監聽變更
- • 適合:已有 PostgreSQL 的團隊