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