
簡介
事件溯源和 CQRS 是兩種經常齊頭並進的模式,有助於解決微服務中的資料一致性、稽核追蹤和效能最佳化等複雜問題。
1. 事件溯源
1.1 概念
不保存當前狀態,而是保存已發生的整個事件序列:
Traditional (State-based):
┌──────────────────────────────┐
│ Orders Table │
│ id: O-001 │
│ status: shipped ← Chỉ biết state hiện tại
│ total: 500,000 │
│ updated_at: 2026-03-31 │
└──────────────────────────────┘
Event Sourcing:
┌──────────────────────────────────────────────────────────┐
│ Event Store (append-only) │
├────┬──────────────────┬──────────────┬──────────────────┤
│ # │ Event Type │ Data │ Timestamp │
├────┼──────────────────┼──────────────┼──────────────────┤
│ 1 │ OrderCreated │ {items, ...} │ 10:00:00 │
│ 2 │ PaymentReceived │ {amount} │ 10:01:00 │
│ 3 │ ItemsReserved │ {items} │ 10:01:05 │
│ 4 │ OrderShipped │ {tracking} │ 10:30:00 │
└────┴──────────────────┴──────────────┴──────────────────┘
Current State = replay(events) → Order{status: "shipped"}
1.2 事件商店
Đặc điểm:
├── Append-only: Không bao giờ update hoặc delete events
├── Immutable: Events là facts đã xảy ra, không thể thay đổi
├── Ordered: Events có thứ tự rõ ràng (sequence number)
└── Stream: Events được nhóm theo aggregate (ví dụ: order-O-001)
Implementation options:
├── EventStoreDB (purpose-built, recommended)
├── PostgreSQL + events table
├── Apache Kafka (log-based)
└── DynamoDB Streams (AWS)
PostgreSQL 事件儲存:
CREATE TABLE events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
stream_id VARCHAR(255) NOT NULL, -- "order-O-001"
version BIGINT NOT NULL, -- sequence number
event_type VARCHAR(255) NOT NULL, -- "OrderCreated"
data JSONB NOT NULL, -- event payload
metadata JSONB DEFAULT '{}', -- traceId, userId, ...
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(stream_id, version) -- đảm bảo ordering
);
CREATE INDEX idx_events_stream ON events(stream_id, version);
1.3 重建狀態
def get_order(order_id: str) -> Order:
events = event_store.get_events(stream_id=f"order-{order_id}")
order = Order() # empty state
for event in events:
order.apply(event) # replay từng event
return order # current state
class Order:
def apply(self, event):
match event.type:
case "OrderCreated":
self.id = event.data["id"]
self.status = "created"
self.items = event.data["items"]
case "PaymentReceived":
self.status = "paid"
case "OrderShipped":
self.status = "shipped"
self.tracking = event.data["tracking"]
1.4 快照優化
當串流中的事件太多(數千個)且重播速度慢時→使用快照:
Event Stream cho order-O-001:
Event 1: OrderCreated
Event 2: ItemAdded
...
Event 500: ItemRemoved
──── Snapshot at version 500 ────
{ status: "processing", items: [...], total: 1000000 }
Event 501: PaymentReceived
Event 502: OrderShipped
Rebuild: Load snapshot (v500) + replay events 501-502
→ Nhanh hơn nhiều so với replay 502 events
1.5 優點和缺點
✅ Ưu điểm:
├── Complete audit trail (ai làm gì, lúc nào)
├── Time travel: Rebuild state tại bất kỳ thời điểm
├── Event replay: Fix bug rồi replay events để sửa data
├── Natural fit cho event-driven architecture
└── Debug: Hiểu chính xác điều gì đã xảy ra
❌ Nhược điểm:
├── Complexity: Khó hơn CRUD đáng kể
├── Query: Không thể query trực tiếp (cần CQRS)
├── Schema evolution: Thay đổi event format phức tạp
├── Storage: Nhiều events → nhiều storage
└── Learning curve: Team cần thời gian adapt
2. CQRS-指令查詢職責分離
2.1 概念
單獨的寫入模型(命令)和讀取模型(查詢):
Traditional:
Client ──CRUD──▶ Same Model ──▶ Same Database
CQRS:
┌─────────────────────────────────────┐
│ API Layer │
└──────────┬───────────────┬───────────┘
│ │
┌──────────▼──────┐ ┌──────▼──────────┐
│ Command Side │ │ Query Side │
│ (Write Model) │ │ (Read Model) │
│ │ │ │
│ - CreateOrder │ │ - GetOrderDetails│
│ - CancelOrder │ │ - ListOrders │
│ - UpdateStatus │ │ - SearchOrders │
└────────┬────────┘ └────────▲─────────┘
│ │
┌────────▼────────┐ ┌───────┴─────────┐
│ PostgreSQL │ │ Elasticsearch │
│ (Write DB) │ │ (Read DB) │
│ Normalized │ │ Denormalized │
└────────┬────────┘ └─────────────────┘
│ ▲
└───── Events ──────┘
(sync read model)
2.2 為什麼要分開讀取和寫入?
Write: Read:
├── Ít operations hơn ├── Nhiều operations hơn (10:1 ratio)
├── Cần ACID consistency ├── Eventual consistency OK
├── Normalized schema ├── Denormalized, pre-joined
├── Complex validation ├── Simple query, fast response
├── Scale: moderate ├── Scale: aggressive (caching, replicas)
└── PostgreSQL (optimal) └── Elasticsearch/Redis (optimal)
2.3 同步讀取模型
Option 1: Domain Events (khuyến nghị)
Write DB ──event──▶ Kafka ──▶ Read Model Updater ──▶ Read DB
Option 2: Change Data Capture (CDC)
Write DB ──Debezium──▶ Kafka ──▶ Read Model Updater ──▶ Read DB
Option 3: Dual Write (KHÔNG khuyến nghị)
Service ──write──▶ Write DB
──write──▶ Read DB ← Có thể inconsistent!
2.4 CQRS + 事件溯源
結合兩種模式:
Command Flow:
Client ──CreateOrder──▶ Command Handler
│
Validate
│
Append event to Event Store
│
Publish event to Kafka
│
▼
Event Store (source of truth)
Query Flow:
Kafka ──OrderCreated──▶ Projection Handler
│
Update Read Model (Elasticsearch)
│
Client ──GetOrder──▶ Query Handler ──▶ Read from Elasticsearch
2.5 最終一致性
Timeline:
T0: Client tạo order (write to Event Store)
T1: Event published to Kafka (~5ms)
T2: Projection handler updates Elasticsearch (~50ms)
T3: Read model available (~100ms after T0)
Giữa T0 và T3: Read model chưa có data mới = Eventual Consistency
Giải pháp UX:
├── Optimistic UI: Client hiển thị ngay sau write, không đợi read model
├── Read-your-writes: Sau write, query write DB cho user đó
├── Polling/WebSocket: Client poll cho đến khi read model updated
└── Inbox pattern: Return 202 Accepted + polling endpoint
3. 何時使用事件溯源/CQRS?
3.1 在下列情況下應使用事件溯源
- ✅ 需要完整的審計追蹤(財務、醫療保健、法律)
- ✅需要時間旅行(隨時重建狀態)
- ✅ 網域具有複雜的狀態轉換(訂單工作流程、預訂)
- ✅ 需要調試生產問題(重播事件)
- ✅ 事件驅動架構已經是基礎
3.2 什麼時候應該使用CQRS?
- ✅ 讀/寫比率差異很大(10:1 或更多)
- ✅ 讀寫需要不同的尺度
- ✅ 讀取需要非規範化/預計算的模型
- ✅ 複雜查詢需要搜尋引擎(Elasticsearch)
3.3 不應在下列情況下使用
- ❌簡單的CRUD應用程式
- ❌團隊沒有事件驅動經驗
- ❌ 域簡單,狀態轉換少
- ❌一致性要求=處處強一致性
- ❌ 期限緊迫,需快速出貨
4. 總結
| 圖案 | 重點 |
|---|---|
| 事件溯源 | 保存事件而不是狀態、僅附加、審計追蹤 |
| 活動商店 | 不可變的事件日誌,真相來源 |
| 快照 | 使用定期快照優化重建 |
| CQRS | 獨立的讀/寫模型,獨立擴展 |
| 最終一致性 | 寫入後讀取模型更新(ms級延遲) |
| 投影 | 處理事件→更新讀取模型 |
下一篇文章:Saga 模式 - 當每個服務都有自己的資料庫時處理分散式交易。