最近想用Scala來重構Java項目。Scala的靈活高效這里就不用說了,Java MVC一套架構確實有它優點。但是開發調試效率確實慢很多。所以准備使用DDD中的命令查詢職責分離模式(Command Query Responsibility Segregation,CQRS)重構項目。
首先我們先介紹下CQRS。
一、什么是CQRS
CQRS最早來自於Betrand Meyer(Eiffel語言之父,開-閉原則OCP提出者)在 Object-Oriented Software Construction 這本書中提到的一種 命令查詢分離 (Command Query Separation,CQS) 的概念。其基本思想在於,任何一個對象的方法可以分為兩大類:
- 命令(Command):不返回任何結果(void),但會改變對象的狀態。
- 查詢(Query):返回結果,但是不會改變對象的狀態,對系統沒有副作用。
根據CQS的思想,任何一個方法都可以拆分為命令和查詢兩部分,比如:
private int i = 0; private int Increase(int value) { i += value; return i; }
這個方法,我們執行了一個命令即對變量i進行相加,同時又執行了一個Query,即查詢返回了i的值,如果按照CQS的思想,該方法可以拆成Command和Query兩個方法,如下:
private void IncreaseCommand(int value) { i += value; } private int QueryValue() { return i; }
操作和查詢分離使得我們能夠更好的把握對象的細節,能夠更好的理解哪些操作會改變系統的狀態。當然CQS也有一些缺點,比如代碼需要處理多線程的情況。
CQRS是對CQS模式的進一步改進成的一種簡單模式。 它由Greg Young在CQRS, Task Based UIs, Event Sourcing agh! 這篇文章中提出。“CQRS只是簡單的將之前只需要創建一個對象拆分成了兩個對象,這種分離是基於方法是執行命令還是執行查詢這一原則來定的(這個和CQS的定義一致)”。
CQRS使用分離的接口將數據查詢操作(Queries)和數據修改操作(Commands)分離開來,這也意味着在查詢和更新過程中使用的數據模型也是不一樣的。這樣讀和寫邏輯就隔離開來了。
使用CQRS分離了讀寫職責之后,可以對數據進行讀寫分離操作來改進性能,可擴展性和安全。如下圖:
主數據庫處理CUD,從庫處理R,從庫的的結構可以和主庫的結構完全一樣,也可以不一樣,從庫主要用來進行只讀的查詢操作。在數量上從庫的個數也可以根據查詢的規模進行擴展,在業務邏輯上,也可以根據專題從主庫中划分出不同的從庫。從庫也可以實現成ReportingDatabase,根據查詢的業務需求,從主庫中抽取一些必要的數據生成一系列查詢報表來存儲。
使用ReportingDatabase的一些優點通常可以使得查詢變得更加簡單高效:
- ReportingDatabase的結構和數據表會針對常用的查詢請求進行設計。
- ReportingDatabase數據庫通常會去正規化,存儲一些冗余而減少必要的Join等聯合查詢操作,使得查詢簡化和高效,一些在主數據庫中用不到的數據信息,在ReportingDatabase可以不用存儲。
- 可以對ReportingDatabase重構優化,而不用去改變操作數據庫。
- 對ReportingDatabase數據庫的查詢不會給操作數據庫帶來任何壓力。
- 可以針對不同的查詢請求建立不同的ReportingDatabase庫。
當然這也有一些缺點,比如從庫數據的更新。如果使用SQLServer,本身也提供了一些如故障轉移和復制機制來方便部署。
三 什么時候可以考慮CQRS
CQRS模式有一些優點:
- 分工明確,可以負責不同的部分
- 將業務上的命令和查詢的職責分離能夠提高系統的性能、可擴展性和安全性。並且在系統的演化中能夠保持高度的靈活性,能夠防止出現CRUD模式中,對查詢或者修改中的某一方進行改動,導致另一方出現問題的情況。
- 邏輯清晰,能夠看到系統中的那些行為或者操作導致了系統的狀態變化。
- 可以從數據驅動(Data-Driven) 轉到任務驅動(Task-Driven)以及事件驅動(Event-Driven).
在下場景中,可以考慮使用CQRS模式:
- 當在業務邏輯層有很多操作需要相同的實體或者對象進行操作的時候。CQRS使得我們可以對讀和寫定義不同的實體和方法,從而可以減少或者避免對某一方面的更改造成沖突
- 對於一些基於任務的用戶交互系統,通常這類系統會引導用戶通過一系列復雜的步驟和操作,通常會需要一些復雜的領域模型,並且整個團隊已經熟悉領域驅動設計技術。寫模型有很多和業務邏輯相關的命令操作的堆,輸入驗證,業務邏輯驗證來保證數據的一致性。讀模型沒有業務邏輯以及驗證堆,僅僅是返回DTO對象為視圖模型提供數據。讀模型最終和寫模型相一致。
- 適用於一些需要對查詢性能和寫入性能分開進行優化的系統,尤其是讀/寫比非常高的系統,橫向擴展是必須的。比如,在很多系統中讀操作的請求時遠大於寫操作。為適應這種場景,可以考慮將寫模型抽離出來單獨擴展,而將寫模型運行在一個或者少數幾個實例上。少量的寫模型實例能夠減少合並沖突發生的情況
- 適用於一些團隊中,一些有經驗的開發者可以關注復雜的領域模型,這些用到寫操作,而另一些經驗較少的開發者可以關注用戶界面上的讀模型。
- 對於系統在將來會隨着時間不段演化,有可能會包含不同版本的模型,或者業務規則經常變化的系統
- 需要和其他系統整合,特別是需要和事件溯源Event Sourcing進行整合的系統,這樣子系統的臨時異常不會影響整個系統的其他部分。
但是在以下場景中,可能不適宜使用CQRS:
- 領域模型或者業務邏輯比較簡單,這種情況下使用CQRS會把系統搞復雜。
- 對於簡單的,CRUD模式的用戶界面以及與之相關的數據訪問操作已經足夠的話,沒必要使用CQRS,這些都是一個簡單的對數據進行增刪改查。
- 不適合在整個系統中到處使用該模式。在整個數據管理場景中的特定模塊中CQRS可能比較有用。但是在有些地方使用CQRS會增加系統不必要的復雜性。
四 CQRS與Event Sourcing的關系
在CQRS中,查詢方面,直接通過方法查詢數據庫,然后通過DTO將數據返回。在操作(Command)方面,是通過發送Command實現,由CommandBus處理特定的Command,然后由Command將特定的Event發布到EventBus上,然后EventBus使用特定的Handler來處理事件,執行一些諸如,修改,刪除,更新等操作。這里,所有與Command相關的操作都通過Event實現。這樣我們可以通過記錄Event來記錄系統的運行歷史記錄,並且能夠方便的回滾到某一歷史狀態。Event Sourcing就是用來進行存儲和管理事件的。這里不展開介紹。
五 CQRS的簡單實現
CQRS模式在思想上比較簡單,但是實現上還是有些復雜。它涉及到DDD,以及Event Sourcing,這里使用codeproject上的 Introduction to CQRS 這篇文章的例子來說明CQRS模式。這個例子是一個簡單的在線記日志(Diary)系統,實現了日志的增刪改查功能。整體結構如下:
上圖很清晰的說明了CQRS在讀寫方面的分離,在讀方面,通過QueryFacade到數據庫里去讀取數據,這個庫有可能是ReportingDB。在寫方面,比較復雜,操作通過Command發送到CommandBus上,然后特定的CommandHandler處理請求,產生對應的Event,將Eevnt持久化后,通過EventBus特定的EevntHandler對數據庫進行修改等操作。