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 CQRSFull 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