共計 7159 個字符,預計需要花費 18 分鐘才能閱讀完成。
這篇文章主要介紹“DDD 里面的 CQRS 是什么”,在日常操作中,相信很多人在 DDD 里面的 CQRS 是什么問題上存在疑惑,丸趣 TV 小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”DDD 里面的 CQRS 是什么”的疑惑有所幫助!接下來,請跟著丸趣 TV 小編一起來學習吧!
開篇
隨著業務不斷發展,軟件系統的架構也越來越復雜,但無論多復雜的業務最終在系統中實現的時候,無非是讀寫操作。用戶根據業務規則寫入商業數據,再根據查詢規則獲取想要的結果。通常而言我們會講這些讀寫的數據放到一個數據庫中保存,通過一套模型對其進行讀寫操作。而在大型系統中往往查詢操作遠遠多于寫入操作,于是就有了讀寫分離的思想,將讀操作和寫操作的模型分開定義并且提供不同的通道供用戶使用。CQRS(Command-Query Responsibility Segregation) 就是基于這一思想提供的一種模式讀寫分離的模式,今天就圍繞著它給大家講述以下內容:
CQRS 的演變和架構
Event Sourcing 原理與應用
Event Sourcing 與 CQRS 的完美結合
CQRS 的例子
CQRS 的演變和架構
CQRS(Command-Query Responsibility Segregation) 是一種讀寫分離的模式,從字面意思上理解 Command 是命令的意思,其代表寫入操作;Query 是查詢的意思,代表的查詢操作,這種模式的主要思想是將數據的寫入操作和查詢操作分開。
它源于 Bertrand Mayer 設計的命令查詢分離 (CQS) 原理。CQS 聲明一個類只能有兩種方法:改變狀態并返回 void 的方法和返回狀態的方法。而 Greg Young 是負責命名這種模式為 CQRS 并推廣它的人。
首先來看看在沒有 CQRS 之前是如何處理系統中的修改和查詢的吧,如圖 1 所示:
圖 1 傳統的系統請求
傳統的系統請求從最左邊的 Client 開始,沿著紅線往右通過 Application Service 對系統進行請求。這里 Application Service 可以理解為系統的門面,或者是 Controller 層負責接收客戶端的請求,此時請求的內容比較簡單基本和數據庫中的信息一致,因此這里使用 DTO(Data Transfer Object)直接請求。DTO 經過 Domain Model 以后直接到達 Database,從而沿著藍色的線條返回給 Client 端。傳統的請求方式部分讀操作和寫操作,都使用同樣的數據模型和一套 Domain Model 以及相同的數據庫。
從傳統操作來看 Client 的請求在經過 Application Service,用戶意圖全部被分解為 CRUD 操作,但是在 Domain Model 中是無法體現的。為保證 DTO 的完整性和一致性,與操作無關的信息會被納入 DTO,查詢操作和創建操作都共用一個 DTO,而領域模型的業務流程被弱化。為了適應同時適應查詢和創建操作,DTO 被設計的面面俱到,也就顯得臃腫。從而在傳輸中存在不必要的字段傳遞。
而且一次操作,在 DTO 與領域對象間進行多次轉換,增加了系統復雜度。還有,讀寫操作將圍繞同一數據模型展開,對于讀多寫少的系統而言效率并不是最高的,特別在讀操作為主的高并發系統中缺點就尤為突出。
正因為傳統系統架構存在上面這些問題,因此 CQRS 根據讀寫職責的不同,把領域模型切分為 Command 端與 Query 端兩個部分,如圖 2 所示,紅色線部分就是 Command 端,其對應的是 Domain Model 對其發送 Command 操作的指令往數據寫入狀態信息。
Query 端作為查詢操作,由藍色的線表示,通過 Query Model 向數據庫獲取信息,通過黑色向左的先返回結果給 Client。Command 端與 Query 端都通過 Application Service 進入系統,共享同一個數據庫,但 Command 端只寫入狀態,Query 端只讀取狀態。
圖 2 CQRS 分為 Command 端和 Query 端
目前而言已經將讀寫操作分開了,由于兩個操作依舊共用一個數據庫,為了提高讀寫效率數據庫的分離就成為必然的選擇。如圖 3 所示,于是將原來的 Database,分離為 Writer Database 和 Reader Database 分別用于寫操作和讀操作。為了保證讀寫操作的數據一致性,需要在兩個數據庫之間進行數據同步。
由于數據同步是由時效性的,因此寫入方是 Command 端,讀取方是 Query 端,因此系統智能保證最終一致性。那么如何保證兩個庫之間的同步呢? 下面需要引入 Event Sourcing 的概念。
Event Sourcing 原理與應用
Event Sourcing 也叫事件溯源,是 Martin Fowler 提出的一種架構模式。其設計思想是系統中的業務都由事件驅動來完成。系統中記錄的是一個個事件,由這些事件體現信息的狀態。業務數據可以是事件產生的視圖,不一定要保存到數據庫中。
為了便于理解 Event Sourcing 我們通過一個例子來進一步解釋,如圖 3 所示:
圖 3 Command 端和 Query 端 讀寫數據庫的分離
我們從左往右看。對于一個業務類“賬戶”,擁有“屬性”包括“賬戶 ID”和“賬戶金額”信息,同時擁有“方法”包括“創建賬戶”、“存現金”和“取現金”。中間綠色的事件序列,是針對“賬戶”進行的一些列操作,按照其中的序列號來看。
1. 創建了一個銀行賬戶,假設此時的賬戶 ID 為“0001”。
2. 針對“0001”這個賬戶存入 300 元現金。
3. 然后從“0001”這個賬戶取出 100 元現金。
4. 最后,再存入 200 元。
上面生成的這一系列事件會保存到下方的 Event Store 的事件庫中,這里并不會保存“賬戶”的狀態信息。當需要獲取“賬戶”數據的時候,會通過這些事件信息,還原成“賬戶”的最終狀態,也就是“賬戶 ID”為“0001”,“賬戶金額”為 400。其具體實現方式是,通過賬戶相關的四個事件對應的處理方法,重新生成當前狀態。如果每次查詢狀態信息都需要這樣處理勢必會造成資源的浪費,因此在右側黃色的部分,我們將最終的“賬戶”信息通過視圖的方式保存下來,以供查詢。
圖 3 Event Sourcing 實例圖
上面這個“賬戶”處理的過程,就是 Event Sourcing,說白了就是通過事件的處理模式。它將系統中的操作都按照事件的方式記錄并保存,任何實體的最終狀態都是通過事件的疊加和還原確認的。
Event Sourcing 包含的內容
上面介紹了 Event Sourcing 的執行原理和基本概念,這里一起來看看其包含的主要內容,便于我們對它有更加全面的理解。
聚合對象:圖 3 的例子中“賬戶”就是一個聚合對象,它里面包含“賬戶 ID”、“賬戶金額”等的基本信息,也包含了對賬戶操作的方法:“創建賬戶”、“存現金”、“取現金”。同時“賬戶”在領域驅動開發中對應的是一個領域模型。
Event Store:在 Event Sourcing 模式中,事件所保存的數據庫稱為 Event Store。在事件中需要包含聚合對象的 ID,以及事件的順序。這樣在查詢的時候可以根據聚合 ID 從數據庫中找到相關的事件,并通過事件的序號還原執行順序。也就是事件的重現,也就是某一時刻執行的事件取出來,調用他的處理函數,還原那個時間點的業務狀態。
為了獲取最新的“賬戶”狀態信息,需要通過 Event Sourcing 中獲取對應的事件進行回放,從而獲取當前的狀態,這樣的操作會浪費很多資源。因此我們會將聚合對象的最新數據狀態,寫到一個表中,這個表就是視圖。又或者將這個狀態信息發送給其他的應用程序進行后續的業務操作。
查詢的內容是針對“賬戶”最終狀態的,因此針對的對象應該是視圖。這里的設定剛好的 CQRS 中的讀寫分離不謀而合,通過 Event Store 存放 Command 端的 Event 信息,通過視圖存放實體最終狀態的信息,而 Query 端從視圖查詢數據返回給用戶。
Event Sourcing 的優缺點
上面介紹了 Event Sourcing 的原理和內容以后再來看看它的優缺點。
Event Sourcing 的優點:
溯源事件與重現操作:特別是在業務復雜的系統中,一個事務包含多個操作,它們有的是并行有的串行,如果需要了解操作的執行就需要對每個事件了如指掌。Event Sourcing 恰恰提供了事件的歷史信息,方便查找任何時間點發生的事情。
追蹤和修復 Bug:可以通過事件分析業務的執行過程,幫助發現 Bug,例如重方 Bug 產生時的事件序列,從而定位 Bug 所處位置。發現 Bug 并且修復以后,可以通過重新聚合業務數據,重放執行的事件序列驗證修復結果,同時將 Bug 造成的損失進行挽回。
提高性能:Event Sourcing 模式下,由于是記錄事件執行的序列,因此都是新增操作,沒有更新操作,相對于需要更新操作的系統而言記錄數據的性能是提高了。如果使用視圖的方式將實體的最終狀態可以傳遞給其他的應用,而不用寫入數據庫以后再讀取,這種做法也提高了效率。
Event Sourcing 的缺點:
轉變思路:Event Sourcing 的落地需要在設計時就用領域驅動的方式開展,需要有基于事件的響應式編程思維。這種方式需要以領域模型設計優先,而不是傳統的數據庫設計優先。
變更事件結構:隨著業務流程的變化需要不斷調整事件結構,對事件添加或者修改一些數據。這種行為會影響到“歷史重現”,需要考慮兼容之前的事件結構。
處理冪等事件:如果對應的事務在執行過程中被中斷,需要通過事件回放的方式達到事務的最終一致性問題。此時需要對事件的冪等性提出要求,也就是同一個事件運行多次得到的結果不變。需要在事件處理時丟棄重復事件。
查詢事件數據庫(event store):由于數據庫中存放的一個個事件,如果針對實體狀態的查詢會相對困難。需要將這些事件重放,獲取最新的實體狀態的信息。這也是為什么需要通過 CQRS 的方式將讀寫進行分離,Command 端使用 Event Sourcing 而 Query 端使用 Event Sourcing 發出 Event 的最終狀態進行查詢的原因。
CQRS 與 Event Sourcing 的 完美結合
通過上面對 Event Sourcing 的介紹,可以發現它針對 Event 進行記錄存放到 Event Store 中,并且把最終的狀態放到視圖中進行保存可以供給 Query 端進行查詢。這種模式天生與 CQRS 就有默契的配合。
從 CQRS 模式的結構看,實體狀態的變化發生在 Command 端,Command 端知道業務處理進行了哪些具體操作,將這些具體的操作進行封裝就形成了 Event。
而 Query 端,查詢返回的是實體當前狀態狀態。根據“當前狀態 + 變化 = 新的狀態”,如果能從 Command 端得到“變化”,再加上 Query 端自身獲取的“當前狀態”就能得到變化后的“新的狀態”。
此時 Command 端發出的 Event 正好符合這個“變化”,如果當變化發生也就是新 Event 產生時,由 Command 端將這個 Event 推送到 Query 端,Query 端根據 Event 刷新狀態,就能保證兩端實體狀態一致,達到最終一致性,如圖 4 所示:
圖 4 Event Sourcing 和 CQRS 結合
在圖 3 的基礎上加入 Event Handler 也就是圖中藍色部分,這部分接收從 Domain Model 中發過來的 Event 信息,也就是最新的實體修改信息。再將這個信息存放到 Reader Database(也可以理解為視圖)中,這樣新的 Event 信息加上當前的實體信息就時最新的實體信息了。而采用這種方式以后 Query 端依舊可以通過 Reader Database 獲取數據對其原來的操作并沒有產生影響。
再回到 Command 端,其對應的多次操作的 Event 會存放到 Event Store 中,作為業務跟蹤的記錄被保存下來。
上面提到的只是一種系統架構的模式,在實際運用中可以根據具體情況進行改進和優化。如圖 5 所示,可以在 Command 端和 Query 端進行 Event 交換的時候加入隊列,滿足兩套應用程序部署在不同進程的場景需求。
圖 5 Command 端和 Query 端加入隊列
一個 CQRS 的例子
上面聊到了 CQRS 與 Event Sourcing 的完美結合,這里通過一個例子給大家進一步介紹其運作的過程。這個例子的背景是,對于用戶 (User) 而言保存了對應的聯系方式(Contact) 和住址(Address)。
Command 用來建立 (Create) 用戶 (User) 和更新(Update) 用戶 (User);Query 用來查詢用戶(User) 對應的住址 (Address) 和聯系方式(Contact)。
如圖 4 所示,Client 請求應用分為上線兩條線,分別用四種顏色代表。我們根據不同顏色來講解 Command 端和 Query 端執行的過程。
圖 4 Event Sourcing 和 CQRS 結合
紅色向左的線:這里主要是針對 User 的 create 和 update 操作,分別填充 CreateUserCommand 類和 UpdateUserCommand 類,作為 UserAggregate 聚合類的輸入參數。在 UserAggregate 中分別由,handleCreateUserCommand 和 handleUpdateUserCommand 兩個方法處理,最后通過 UserWriteRepository 來保存到 Write database 中。
綠色向下的線:其連接了紫色的區域是 UserProjection,它的作用是將 Write database 的數據同步到 Read database 中。
藍色向右的線:Client 發起 Query 請求通過 AddressByRegionQuery 類和 ContactByTypeQuery 類構建請求,將其傳送到 UserProjection 類進行處理,其中 handle 方法分別對兩類參數的請求進行處理。最后通過 UserReadRepository 獲取 Read database 中的信息。
紫色向左的線:當從 Read database 中獲取信息以后,返回給 Client。
圖 6 CQRS 例子圖解
在了解了整體架構以后再來看看具體實現的類結構。
如圖 7 所示,User 實體類包括如下幾個字段,也就是我們要操作的業務實體。包括用戶的基本信息,其中 contact 和 address 類的具體信息在這里不展開描述。
圖 7 User 實體類
Command 的類信息如圖 8 所示,其內容相對簡單。針對 CreateUserCommand 主要用于創建用戶,包括 UserID 和 FirstName 以及 LastName。
圖 8 CreateUserCommand 類
如圖 9 所示,UpdateUserCommand 中加入了地址和聯系方式的更新內容。
圖 9 UpdateUserCommand 類
有了 Command 再來看看聚合類 UserAggregate,由于其中包括 Create 和 Update 的處理方法,這里介紹其中的 handleCreateUserCommand 方法,也就是處理新建用戶命令。
這里會創建一個 UserCreatedEvent 對象,并將其通過 WriteRepository 保存到 Write database 中。也就是在 ES 中的 Event store,同時會將 event 的 list 返回。
圖 10 handleCreateUserCommand 類
在處理完 Command 以后會返回 Event,這個 Event 在保存到數據庫中的同時,也會發送和 Query 端作為最新的實體狀態進行更新,這里會用到 UserProjector 類完成映射。如圖 11,所示,其中的 project 方法會針對 UserID 的 events 進行逐一處理。
圖 11 UserProjector 類
看完了 Command 端和 同步的 Projector,再來看看 Query 端的類。如圖 12 所示,AddressByRegionQuery 類定義了 UserID 和 State 信息。
圖 12 AddressByRegionQuery 類
如圖 13 所示,ContactByTypeQuery 定義了 UserID 和 ContactType 的信息。
圖 13 ContactByTypeQuery 類
如圖 14 所示,上面提到的 AddressByRegionQuery 和 ContactByTypeQuery 作為參數傳入到 UserProjection 類的 handle 方法中,并且返回對應的 Contact 和 Address 信息。使用了 UserReadRepositiory 從 Read database 中獲取數據。
圖 14 UserProjection
最后,再來看看測試代碼這里將其分為 7 個步驟,如圖 15 所示。
隨機生成用戶 ID。
鴻蒙官方戰略合作共建——HarmonyOS 技術社區
通過 CreateUserCommand,創建新建用戶的 Command,并且通過 UserAggregate 生成對應的事件。
通過 UserProjector 將事件映射到 Query 端的數據庫中。
通過 UpdateUserCommand,創建更新地址信息的 Command,生成對應的事件。
通過 UserProjector 將事件映射到 Query 端的數據庫中。
通過 AddressByRegionQuery,創建查詢地址信息的 Query。
執行查詢從 Read database 中獲取數據與假設值進行比較。
圖 15 Command 和 Query 的執行過程
最后來看看這些文件的目錄結構,如圖 16 所示。
圖 16 文件結構
到此,關于“DDD 里面的 CQRS 是什么”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注丸趣 TV 網站,丸趣 TV 小編會繼續努力為大家帶來更多實用的文章!