您好,登錄后才能下訂單哦!
這篇文章主要介紹“DDD里面的CQRS是什么”,在日常操作中,相信很多人在DDD里面的CQRS是什么問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”DDD里面的CQRS是什么”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
開篇
隨著業務不斷發展,軟件系統的架構也越來越復雜,但無論多復雜的業務最終在系統中實現的時候,無非是讀寫操作。用戶根據業務規則寫入商業數據,再根據查詢規則獲取想要的結果。通常而言我們會講這些讀寫的數據放到一個數據庫中保存,通過一套模型對其進行讀寫操作。而在大型系統中往往查詢操作遠遠多于寫入操作,于是就有了讀寫分離的思想,將讀操作和寫操作的模型分開定義并且提供不同的通道供用戶使用。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是什么”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。