讀和寫是兩種不同的工作,憑什麼共用同一個模型?CQRS 把寫模型(command)與讀模型(query)拆開,各自針對自己的負載與形狀最佳化。代價是兩者之間多了一段最終一致的同步。
🧠 Intuition
TIP
一份正規化的寫模型保證資料正確;一份(或多份)去正規化的讀模型保證查詢快。寫模型負責「能不能改」,讀模型負責「好不好查」,中間用事件或投影把寫的變更同步到讀。
- Command 端:處理寫入、執行業務規則與不變量驗證,通常是正規化、為一致性最佳化的模型。
- Query 端:服務讀取,是去正規化、按 UI/查詢形狀預先塑形的讀模型,可有多個(搜尋、報表、列表各一份)。
- 兩端透過事件或變更流連接,讀模型是物化視圖 (materialized view):把查詢結果預先算好存起來,讀時直接拿,不臨場 join/聚合。
- 因為同步需要時間,讀端是最終一致——寫完不保證立刻在讀模型看到。
⚖️ Tradeoffs
為何要分 — 讀寫負載與形狀都不同
- 多數系統讀遠多於寫,分開後可以各自獨立擴展(讀端加 replica、寫端保持單一致性源)。
- 寫要的是正規化以維護不變量;讀要的是去正規化以避免昂貴 join。同一個模型同時滿足兩者,必然有一方被犧牲。
- 分開後讀模型可以針對特定查詢量身打造,甚至用不同儲存引擎(寫進 RDBMS、讀走 Elasticsearch)。
物化視圖 / 讀投影 — 預先算好的答案
- 讀模型本質是 materialized view:把 join、聚合、去正規化的結果事先算好落地,把成本從讀時搬到寫時。
- 讀投影 (read projection) 透過訂閱寫端的事件流、增量更新自己,而不是每次全量重算。
- 同一份事實來源可以投影出任意多個讀模型,需求變了就新增一個投影,不動寫端。
最終一致的讀端 — 必須面對的現實
- 寫到讀有同步延遲,使用者可能寫完馬上讀卻看不到自己的更新(read-your-writes 問題)。
- 緩解:UI 樂觀更新、寫完短暫讀寫端或 version 標記、明確告知使用者「處理中」。
- 不能接受任何 staleness 的場景(強一致讀),CQRS 反而是負擔,別硬上。
與 Event Sourcing 的關係 — 常一起但不綁定
- CQRS 和事件溯源是天作之合:事件流天然就是更新讀投影的來源,但兩者互相獨立。
- 可以只用 CQRS 不用事件溯源:寫端照常 CRUD,靠 CDC 或應用層發事件去更新讀模型。
- 也可以只用事件溯源不嚴格分讀寫。把兩者當成必須綁定的套餐,是常見誤解。
🔑 Takeaways
- CQRS 的核心是承認「讀和寫是不同工作」:寫模型保正確(正規化),讀模型保快(去正規化物化視圖),用事件/投影連接。
- 讀模型是 materialized view——把 join/聚合成本從讀時搬到寫時,可投影出多份、用不同儲存引擎,代價是最終一致。
- 它和事件溯源常一起出現(事件流是更新投影的理想來源)但並不綁定,可單獨使用任一個。
- 別預設全系統套用:沒有讀寫不對稱就是純增複雜度,先單模型,痛了再拆局部。
2026-07-01
- Q: CQRS 聽起來很強,是不是預設就該把系統都拆成讀寫兩端?
- A: 不是,這是最常見的過度使用陷阱。CQRS 引入了兩個模型、一條同步管線、以及最終一致性帶來的整套複雜度(延遲、read-your-writes、投影重建、監控同步落後)。只有當讀寫負載差異大、或讀的查詢形狀和寫的模型嚴重不匹配、需要獨立擴展或多種讀視圖時,這些成本才划算。對 CRUD 就能滿足的一般業務系統,單一模型遠比 CQRS 好維護。實務上更常見的是只在單一 bounded context 內、確實有讀寫不對稱的局部採用 CQRS,而不是整個系統一刀切。先用最簡單的單模型,等讀寫真的打架了再拆。