Greg Young 在 2010 年提出 CQRS 時,他描述的是一個具體問題:同一個 domain model 同時要處理「改資料」和「查資料」,兩件事的要求完全不同——寫入需要強一致性和業務規則驗證,讀取需要高效能和彈性的 projection。一個 model 兼顧兩者,結果是兩件事都做得很痛苦。
CQRS 的答案是把它們分開。Command(寫入)走一條路,Query(讀取)走另一條路,各自用最適合的模型。
讀寫衝突從哪裡來
電商訂單系統是個清楚的例子。寫入訂單時,你需要的是嚴格的業務規則——庫存夠不夠、用戶信用額度夠不夠、優惠碼有沒有用過——這些規則需要規範化的(normalized)資料結構才好維護。
但查詢「這個用戶的訂單列表,含商品名稱、封面圖片、配送狀態」時,你需要跨越 orders、products、images、shipments 四張表的資料。每次查詢都 JOIN 四張表,效能差;把 JOIN 的結果 cache 起來,cache invalidation 又是另一個問題;如果為了查詢效能把資料 denormalize 進 orders 表,寫入邏輯又要同步更新那些冗餘欄位。
讀模型和寫模型的需求天生衝突——CQRS 讓你不必在一個 model 裡兩邊妥協。
Simple CQRS vs Full CQRS
CQRS 不是一個全有或全無的設計,有兩種完全不同規模的實作:
Simple CQRS:同一個資料庫,只是在程式碼層面把讀和寫分開。Command handler 負責驗證和寫入,Query handler 負責讀取——但它們操作的是同一個 DB schema。這是最低成本的 CQRS,帶來程式碼組織上的好處,幾乎沒有額外的維運負擔。
Full CQRS:獨立的讀資料庫(Read Store)。寫入走 Write Model,然後把變更同步到另一個針對查詢優化的 Read Store(可能是已 denormalize 的 PostgreSQL view、Elasticsearch、Redis 的預計算結果)。讀取直接打 Read Store,不用管 Write Model 的 schema。
| 維度 | Simple CQRS | Full CQRS |
|---|---|---|
| 複雜度 | 低(只是 code 分開) | 高(多一個 Read Store + 同步機制) |
| 效能提升 | 有限(同一個 DB) | 顯著(讀可以用最適合的資料形式) |
| 一致性 | 強一致 | Eventual consistency |
| 維運 | 無額外負擔 | 需要管 Read Store 同步、lag 監控 |
Eventual Consistency 的代價
Full CQRS 引入了一個不可避免的問題:用戶剛寫入的資料,在 Read Store 同步完成之前,查詢可能看不到。
寫入訂單成功 → 系統把 event 推給 Read Store → Read Store 更新 → 用戶查詢
這個「從寫入到查詢可見」的視窗期可能是幾毫秒,也可能在系統壓力下拉長到幾秒。用戶按下「送出訂單」然後立刻查訂單列表,有可能看不到剛送出的那筆。
這在很多場景是可以接受的——社群平台的 feed 更新延遲幾秒沒有人在意。但在電商訂單、金融交易等場景,「我明明付款了但查不到」會讓用戶打電話來客服。
引入 Full CQRS 之前,這個問題要有明確的答案:你的用戶能接受多長的視窗期?你的前端準備好怎麼處理這段期間的 UI 狀態?
CQRS 在 Pattern 圖譜裡的位置
CQRS 和 Event Sourcing 經常一起出現,但它們不互相依賴。
Event Sourcing 記錄了所有改變系統狀態的 event,這些 event 天然地可以用來建立不同的 Read Model(projection)——把 event stream 重播一遍,就能得到任何你需要的讀取視角。從這個角度看,Event Sourcing 讓 Full CQRS 的 Read Store 同步變得更有結構:每個狀態變更都有對應的 event,Read Store 只需要訂閱 event stream 並更新自己。
反過來,CQRS 不要求 Event Sourcing。你可以用 Change Data Capture(CDC)把 Write DB 的變更同步到 Read Store,完全不用 event sourcing 的概念。
CQRS 和 Saga 的關係也值得一提:在 Saga 執行期間,用戶查詢狀態是一個天然的讀寫分離場景。Saga 正在改變 Write Model 的狀態(訂單從 PENDING 到 CONFIRMED),而 Read Model 可以獨立維護一個「用戶可見的訂單狀態」,讓 Saga 還在執行時用戶也有東西可以看。
什麼時候不需要 CQRS
這個問題比「什麼時候需要」更重要,因為 CQRS 被過度使用的頻率遠高於被低估的頻率。
讀寫比接近 1:1 的系統。CQRS 的價值來自讀寫需求的不對稱——如果你的系統每次寫入都被差不多數量的讀取,讀寫 model 的需求通常也接近,分離的收益有限。
讀取邏輯簡單的 CRUD 系統。如果你的查詢就是 SELECT * FROM orders WHERE user_id = ?,不需要跨表 JOIN 或複雜 aggregation,一個 model 就夠了。
團隊對 eventual consistency 的影響沒有準備好。Full CQRS 帶來的 eventual consistency 不只是技術問題,是產品設計和用戶體驗設計的問題。如果產品設計沒有為視窗期做出明確決策,引入 CQRS 會在不預期的地方出現 bug。
系統還小、流量還低。CQRS 是為了解決規模問題存在的。在規模問題還沒出現時引入,只是在增加系統複雜度,沒有實質回報。
CQRS 的本質是一個取捨:用維運複雜度(Read Store 同步、eventual consistency 處理)換取讀寫的各自最優化。這個取捨在正確的場景非常值得,在錯誤的場景是純粹的負擔。
Simple CQRS 幾乎沒有理由不用——把讀和寫在程式碼層面分開,讓職責清楚,這是好的架構紀律,不是過度工程。Full CQRS 需要明確的理由:讀寫比極度不對稱、讀模型需要 denormalize、或者讀取需要完全不同的儲存引擎。
上一篇 → Saga Pattern
下一篇 → Event Sourcing