了解一下CQRS模式(cqrs原則)
背景問(wèn)題
簡(jiǎn)單的需求
當(dāng)我們系統(tǒng)中的數(shù)據(jù)模型層級(jí)較少時(shí),數(shù)據(jù)模型足夠簡(jiǎn)單時(shí),模型與數(shù)據(jù)庫(kù)可以直接進(jìn)行映射。這種簡(jiǎn)單數(shù)據(jù)模型使我們不需要針對(duì)其相互關(guān)系進(jìn)行復(fù)雜的建模設(shè)計(jì),直接在工程中使用經(jīng)典的三層模型就足以支撐項(xiàng)目需求。
對(duì)于這種簡(jiǎn)單系統(tǒng),過(guò)度設(shè)計(jì)會(huì)增加后續(xù)維護(hù)、重構(gòu)的成本(并不能保證預(yù)設(shè)計(jì)能完美符合后續(xù)需求)。同時(shí),對(duì)于簡(jiǎn)單系統(tǒng),我們大部分的需求都只涉及其中的少量數(shù)據(jù)模型邏輯處理。
而我們直接對(duì)數(shù)據(jù)模型進(jìn)行CURD就能滿足需求,進(jìn)而的結(jié)論就是:
針對(duì)簡(jiǎn)單需求,我們不需要特別區(qū)別查詢和增刪改的程序結(jié)構(gòu)。
復(fù)雜的需求
如果我們的系統(tǒng)具有一定復(fù)雜性,這種復(fù)雜性可能是源于訪問(wèn)頻次、數(shù)據(jù)量或者是數(shù)據(jù)模型數(shù)量。這時(shí)候我們遇到的問(wèn)題是數(shù)據(jù)在查詢和更新的需求差距逐漸變大。
- 頻次:數(shù)據(jù)的查詢頻次會(huì)遠(yuǎn)高于新增、更新、刪除頻次。
- 數(shù)據(jù)量:數(shù)據(jù)量變大后會(huì)增加對(duì)數(shù)據(jù)進(jìn)行分庫(kù)分表的設(shè)計(jì)訴求,從而導(dǎo)致數(shù)據(jù)查詢變得的復(fù)雜性(涉及分表關(guān)鍵字)。
- 數(shù)據(jù)模型數(shù)量:數(shù)據(jù)模型數(shù)量的增大,會(huì)導(dǎo)致在進(jìn)行新增、更新與刪除操作時(shí)同時(shí)影響的數(shù)據(jù)模型變多,而在查詢時(shí)同時(shí)跨多模型的查詢條件會(huì)讓查詢的性能具有極大的挑戰(zhàn)性。
根據(jù)以上舉例我們可以發(fā)現(xiàn),當(dāng)我們的需求具有一定的復(fù)雜性后,根據(jù)引入復(fù)雜性的不同,會(huì)導(dǎo)致系統(tǒng)功能上需要用更加復(fù)雜的設(shè)計(jì)來(lái)對(duì)需求的復(fù)雜性進(jìn)行支撐。同時(shí)我們也可以發(fā)現(xiàn),引入的不同復(fù)雜性在增刪改和查詢方面的帶來(lái)的功能需求差別很大。
所以:
需求的復(fù)雜性會(huì)放大程序中查詢和增刪改的設(shè)計(jì)差異。
DDD的需求
如果我們對(duì)系統(tǒng)整體的構(gòu)建與設(shè)計(jì)有了更高的可維護(hù)性與可擴(kuò)展性要求,以至于我們需要使用DDD來(lái)設(shè)計(jì)整個(gè)系統(tǒng)。
在這種情況下往往模型中具有相對(duì)復(fù)雜的模型關(guān)系,在增刪改時(shí)我們需要將所有請(qǐng)求封裝為領(lǐng)域?qū)ο螅员愠绦蚩梢曰?span id="fl3hpth" class="candidate-entity-word" data-gid="14344012">領(lǐng)域模型完成大量復(fù)雜的校驗(yàn)、業(yè)務(wù)邏輯。而在查詢需求時(shí),我們常常需要組織跨領(lǐng)域數(shù)據(jù)來(lái)完成一個(gè)列表中數(shù)據(jù)內(nèi)容的展示。所以:
在DDD設(shè)計(jì)中,增刪改操作便于應(yīng)用領(lǐng)域模型執(zhí)行,而查詢操作往往無(wú)法直接通過(guò)領(lǐng)域模型執(zhí)行。
CQRS模式
問(wèn)題的抽象
根據(jù)第一節(jié)中的內(nèi)容我們可以發(fā)現(xiàn),在進(jìn)行系統(tǒng)架構(gòu)設(shè)計(jì)時(shí),當(dāng)系統(tǒng)出現(xiàn)復(fù)雜性后存在一個(gè)核心問(wèn)題:
增刪改類(lèi)型的功能與查詢類(lèi)型的功能,在功能需求上具有較大的差異。
這種差異帶來(lái)的直接結(jié)果就是在系統(tǒng)開(kāi)發(fā)的過(guò)程中,針對(duì)增刪改和查詢操作的業(yè)務(wù)設(shè)計(jì)上差異會(huì)比較大。如果舉幾個(gè)例子來(lái)說(shuō)的話,比如:
- 針對(duì)增刪改系統(tǒng)我們需要事務(wù)來(lái)保證多領(lǐng)域模型的更新原子性;針對(duì)查詢我們需要增加緩存來(lái)提高熱點(diǎn)數(shù)據(jù)的查詢性能。
- 數(shù)據(jù)讀取和寫(xiě)入的模型通常是不匹配的,他們維護(hù)和查詢的列或者屬性坑沒(méi)有交集。
- 在更新的時(shí)候查詢數(shù)據(jù)可能會(huì)產(chǎn)生沖突。
- 使用統(tǒng)一模型進(jìn)行存儲(chǔ)可能會(huì)導(dǎo)致復(fù)雜查詢時(shí)的性能降低。
CQRS本質(zhì)
由于存在增刪改與查詢邏輯有差異的這個(gè)問(wèn)題,為了更好的針對(duì)差異進(jìn)行抽象,我們可以將它們分開(kāi)進(jìn)行設(shè)計(jì)。也就是我們的CQRS模式,即命令查詢的責(zé)任分離Command Query Responsibility Segregation模式。其中我們稱(chēng)增刪改為命令型操作。
CQRS本質(zhì)上是一種讀寫(xiě)分離設(shè)計(jì)思想,這種框架設(shè)計(jì)模式將命令型業(yè)務(wù)和查詢型業(yè)務(wù)分開(kāi)單獨(dú)處理。通過(guò)這種方式,CQRS可以針對(duì)命令和查詢單獨(dú)進(jìn)行業(yè)務(wù)模型上的設(shè)計(jì),從而用更加適合各自場(chǎng)景的方案與組件來(lái)提供能力。
查詢
查詢操作并不會(huì)修改數(shù)據(jù)庫(kù)中的內(nèi)容,所以查詢本身是一種冪等操作,以同一個(gè)查詢條件在系統(tǒng)不改變的情況下反復(fù)執(zhí)行會(huì)返回相同的結(jié)果,我們可以針對(duì)這種特性提供數(shù)據(jù)緩存來(lái)提高系統(tǒng)性能;同時(shí)因?yàn)椴挥绊憯?shù)據(jù)庫(kù),查詢邏輯是不會(huì)產(chǎn)生數(shù)據(jù)一致性問(wèn)題。查詢往往會(huì)存在較高的使用頻率。
命令 命令操作會(huì)直接修改數(shù)據(jù)庫(kù),并針對(duì)多個(gè)領(lǐng)域模型的情況下我們需要增加來(lái)保證操作的原子性。而對(duì)于一個(gè)命令操作,我們往往是不直接依賴(lài)命令的返回值的,所以通??梢援惒綀?zhí)行命令操作。對(duì)于一般系統(tǒng)來(lái)說(shuō),往往命令操作的使用頻次會(huì)較低。
簡(jiǎn)單實(shí)用
由于CQRS的本質(zhì)是對(duì)于讀寫(xiě)操作的分離,所以比較簡(jiǎn)單的CQRS的做法是:
CQ兩端數(shù)據(jù)庫(kù)表共享,CQ兩端只是在上層代碼上分離。
這種做法在不對(duì)數(shù)據(jù)庫(kù)進(jìn)行分離設(shè)計(jì)的情況下,CQ兩端在上層代碼進(jìn)行分離個(gè)字單獨(dú)維護(hù),例如命令型的都用xxxManagerController、xxxManagerService來(lái)定義,而查詢則直接用xxxController、xxxService定義。
因?yàn)槭褂猛粋€(gè)數(shù)據(jù)庫(kù),所以沒(méi)有CQ兩端的數(shù)據(jù)一致性問(wèn)題。但因?yàn)橐呀?jīng)對(duì)上層代碼進(jìn)行了抽離,所以可以滿足一些設(shè)計(jì)特性如:
- 命令應(yīng)基于任務(wù),而不是以數(shù)據(jù)為中心。
- 命令可以放置在隊(duì)列上進(jìn)行異步處理,而不是同步處理。
- 查詢從不修改數(shù)據(jù)庫(kù)。 查詢返回的 DTO 不封裝任何域知識(shí)。
這種方案可以滿足代碼邏輯上的分離維護(hù),但由于是使用同一數(shù)據(jù)庫(kù)表,所以無(wú)法根據(jù)CQ兩種業(yè)務(wù)的特點(diǎn)單獨(dú)進(jìn)行模型設(shè)計(jì)。
關(guān)注性能
在代碼分離的基礎(chǔ)上,我們可以再將數(shù)據(jù)存儲(chǔ)的模型進(jìn)行物理分離,讀取存儲(chǔ)可以是寫(xiě)入存儲(chǔ)的只讀副本,使用多個(gè)只讀副本可以提高查詢性能;也可能為讀取模型單獨(dú)設(shè)計(jì)庫(kù)表。單獨(dú)對(duì)查詢和更新進(jìn)行模型設(shè)計(jì)可以減小設(shè)計(jì)和實(shí)現(xiàn)的難度。并且此時(shí)讀取數(shù)據(jù)庫(kù)可使用自己的已針對(duì)查詢進(jìn)行優(yōu)化的數(shù)據(jù)架構(gòu)。比如讀數(shù)據(jù)庫(kù)可以直接存儲(chǔ)查詢數(shù)據(jù)寬表從而避免進(jìn)行join操作或者復(fù)雜的查詢映射。甚至可以針對(duì)讀取操作使用mongo或者es等nosql數(shù)據(jù)庫(kù)對(duì)查詢邏輯進(jìn)行增強(qiáng)。
分離后的數(shù)據(jù)將存在在不同的數(shù)據(jù)庫(kù)中,Q的數(shù)據(jù)由C端同步過(guò)來(lái)。通常,這是通過(guò)在每次更新數(shù)據(jù)庫(kù)時(shí)使寫(xiě)入模型發(fā)布事件來(lái)實(shí)現(xiàn)的。 而說(shuō)到數(shù)據(jù)同步則就有同步執(zhí)行和異步執(zhí)行兩種方案:
- 同步:導(dǎo)致性能降低,但是可以保證數(shù)據(jù)的強(qiáng)一致性。
- 異步:擁有較高的性能,但需要系統(tǒng)接受最終一致性的。
同樣的,這種同步也可以解釋為對(duì)緩存進(jìn)行的更新,即:查詢數(shù)據(jù)庫(kù)是使用緩存,而寫(xiě)入數(shù)據(jù)庫(kù)使用普通MySQL,兩者之間數(shù)據(jù)同步通過(guò)領(lǐng)域事件實(shí)現(xiàn)最終一致性。
進(jìn)一步強(qiáng)化
進(jìn)一步的,由于命令操作實(shí)際上是對(duì)“操作”進(jìn)行的記錄,而只有查詢才需要將所有的操作進(jìn)行匯總展示?;谶@種思想,可以使用事件溯源EventSourcing模式來(lái)進(jìn)行命令操作的記錄。在這種方案下,保存記錄時(shí)更新的不是當(dāng)前的記錄,而是會(huì)導(dǎo)致?tīng)顟B(tài)變化的事件日志,每個(gè)事件表示對(duì)數(shù)據(jù)所作的一系列更改,而我們可以通過(guò)重播事件構(gòu)造數(shù)據(jù)當(dāng)前的狀態(tài)(可以參考Mysql的Binlog設(shè)計(jì))。這種記錄的優(yōu)點(diǎn)是可以根據(jù)回放,重現(xiàn)每一次狀態(tài)變更的時(shí)間點(diǎn)以及變更軌跡。而查詢則可以根據(jù)當(dāng)前狀態(tài)的快照來(lái)為查詢提速。來(lái)自于網(wǎng)絡(luò)的架構(gòu)圖:
這種設(shè)計(jì)模式聽(tīng)起來(lái)就比較復(fù)雜,但是卻有很多好處,例如:實(shí)現(xiàn)透明的分布式處理,當(dāng)使用事件作為狀態(tài)改變的引擎時(shí),你可以通過(guò)實(shí)現(xiàn)多任務(wù)并發(fā)處理,比如通過(guò)JVM并行計(jì)算或事件消息總線機(jī)制,事件能夠很容易序列化,并在多個(gè)服務(wù)器之間傳送。同時(shí)因?yàn)槭潜A舻牟僮饔涗?,可以在回放的時(shí)候?qū)τ诋惓2僮鲾?shù)據(jù)進(jìn)行過(guò)濾,從而增加了數(shù)據(jù)的魯棒性。
使用挑戰(zhàn)
如果希望使用CQRS,根據(jù)你希望實(shí)現(xiàn)的系統(tǒng)性能,你需要評(píng)估當(dāng)前系統(tǒng)架構(gòu)以及個(gè)人經(jīng)驗(yàn)是否有以下能力:
- 復(fù)雜性設(shè)計(jì):盡管CQRS基礎(chǔ)理念較為容易理解,但是這種模式會(huì)導(dǎo)致系統(tǒng)的構(gòu)建復(fù)雜度上升,尤其是進(jìn)一步使用事件溯源模式時(shí)。
- 消息隊(duì)列處理:在進(jìn)行高性能設(shè)計(jì)的時(shí)候,通常會(huì)使用消息處理命令和發(fā)布更新事件。在此情況下,應(yīng)用程序必須處理消息失敗或重復(fù)的消息。
- 最終一致性:如果分離讀取和寫(xiě)入數(shù)據(jù)庫(kù),讀取數(shù)據(jù)可能會(huì)過(guò)時(shí)。 必須更新讀取模型存儲(chǔ),以反映對(duì)寫(xiě)入模型存儲(chǔ)區(qū)所做的更改,并且在用戶根據(jù)過(guò)時(shí)的讀取數(shù)據(jù)發(fā)出請(qǐng)求時(shí),可能很難檢測(cè)到這種情況。
選型建議
對(duì)于以下場(chǎng)景不建議引入CQRS:
- 領(lǐng)域或者業(yè)務(wù)十分簡(jiǎn)單。
- 基本的CRUD就可以支撐完整的系統(tǒng)數(shù)據(jù)訪問(wèn)需求。
如果系統(tǒng)存在一定的復(fù)雜性,并且有以下的特點(diǎn),則可以根據(jù)特點(diǎn),選擇適合的CQRS實(shí)現(xiàn)方式。
- 在用戶操作中,需要在用戶界面中進(jìn)行一系列的復(fù)雜操作來(lái)最終定義、組裝、修改領(lǐng)域模型。寫(xiě)模型需要有完成的命令處理堆棧,包括:輸入驗(yàn)證、業(yè)務(wù)處理、業(yè)務(wù)驗(yàn)證。而讀模型只需要返回視圖中所用到的DTO數(shù)據(jù)。讀模型與寫(xiě)模型只需要最終一致性關(guān)系。
- 對(duì)于用戶的操作訪問(wèn),需要以較小的粒度定義命令,并通過(guò)合并命令的方式避免命令沖突。
- 數(shù)據(jù)寫(xiě)入和數(shù)據(jù)讀取之前存在比較大的性能區(qū)別,需要分開(kāi)進(jìn)行數(shù)據(jù)優(yōu)化。尤其是讀取次數(shù)遠(yuǎn)大于寫(xiě)入次數(shù)的場(chǎng)景,可以對(duì)讀模型進(jìn)行水平擴(kuò)展。
- 當(dāng)團(tuán)隊(duì)人員可以分拆分,組成專(zhuān)門(mén)針對(duì)復(fù)雜業(yè)務(wù)寫(xiě)場(chǎng)景的組,以及專(zhuān)門(mén)針對(duì)高頻查詢和用戶界面的組。
- 當(dāng)系統(tǒng)隨時(shí)間不斷演進(jìn),不斷包含多個(gè)版本的模型,或者業(yè)務(wù)規(guī)則會(huì)定期修改??梢栽趯?xiě)模式中包含多個(gè)版本的模型,而讀模式中使用統(tǒng)一的視圖模型。
- 與其他系統(tǒng)集成時(shí),希望不會(huì)受到其他系統(tǒng)故障的影響(讀寫(xiě)庫(kù)表分離)。
最后
總的來(lái)說(shuō),CQRS是處理復(fù)雜問(wèn)題的一種具體實(shí)現(xiàn)方案,常用于配合DDD使用。
總結(jié)CQRS 的主要優(yōu)點(diǎn)包括:
- 獨(dú)立縮放:CQRS 允許讀取和寫(xiě)入工作負(fù)載獨(dú)立縮放,這可能會(huì)減少鎖爭(zhēng)用。
- 優(yōu)化的數(shù)據(jù)架構(gòu): 讀取端可使用針對(duì)查詢優(yōu)化的架構(gòu),寫(xiě)入端可使用針對(duì)更新優(yōu)化的架構(gòu)。
- 安全性:更輕松地確保僅正確的域?qū)嶓w對(duì)數(shù)據(jù)執(zhí)行寫(xiě)入操作。
- 關(guān)注點(diǎn)分離:分離讀取和寫(xiě)入端可使模型更易維護(hù)且更靈活。 大多數(shù)復(fù)雜的業(yè)務(wù)邏輯被分到寫(xiě)模型。 讀模型會(huì)變得相對(duì)簡(jiǎn)單。
- 查詢更簡(jiǎn)單:通過(guò)將具體化視圖存儲(chǔ)在讀取數(shù)據(jù)庫(kù)中,應(yīng)用程序可在查詢時(shí)避免復(fù)雜聯(lián)接。