如何寫(xiě)出簡(jiǎn)潔的 CQRS 代碼?
命令和查詢(xún)責(zé)任分離(CQRS)是指將數(shù)據(jù)存儲(chǔ)的讀取和更新操作分開(kāi)的一種模式。實(shí)施 CQRS 據(jù)稱(chēng)可以提高性能、可擴(kuò)展性和安全性。遷移到 CQRS 模式所創(chuàng)造的靈活性,使系統(tǒng)能夠隨著時(shí)間的推移而更好地發(fā)展。不過(guò) CQRS 模式有一些眾所周知的陷阱,本文介紹了三種實(shí)用的場(chǎng)景。
CQRS 模式可以創(chuàng)造奇跡:它可以將可擴(kuò)展性、性能、安全性最大化,甚至可以打破 CAP 定理 (1)。盡管如此,CQRS 還是因?yàn)槠湟氲膹?fù)雜性而獲得了一個(gè)有爭(zhēng)議的名字。例如,Martin Fowler 在其 CQRS 文章 (2) 中認(rèn)為,應(yīng)該少用甚至謹(jǐn)慎地應(yīng)用該模式。
- 對(duì)大多數(shù)系統(tǒng)來(lái)說(shuō),CQRS 增加了風(fēng)險(xiǎn)的復(fù)雜性
- 你應(yīng)該非常謹(jǐn)慎地使用 CQRS
- 雖然 CQRS 是工具箱中的一種模式,但要注意的是,它很難用得好,如果處理不當(dāng),很容易搞壞你重要的部件。
從我的觀點(diǎn)來(lái)看,CQRS 帶來(lái)的復(fù)雜性在很大程度上是偶然的,也是可避免的。為了說(shuō)明我的觀點(diǎn),我想先討論一下 CQRS 的目標(biāo),然后分析一下基于CQRS 系統(tǒng)中常見(jiàn)的三個(gè)復(fù)雜性的來(lái)源。
CQRS 的目標(biāo)
CQRS 的目標(biāo)是使用多種模型來(lái)表示相同的數(shù)據(jù),與可擴(kuò)展性、可用性、安全性、性能都沒(méi)有關(guān)系。在多個(gè)模型中表示相同的數(shù)據(jù),這就是目標(biāo),剩下的都是副產(chǎn)品。不信?聽(tīng)聽(tīng) Greg Young 在 DDDEU2016 大會(huì)上的演講 (3) ,他說(shuō) CQRS 是為了支持 Event Sourcing (事件溯源)實(shí)現(xiàn)而發(fā)明的。而且大家可能都知道,Event Sourcing 模型對(duì)于寫(xiě)數(shù)據(jù)來(lái)說(shuō)很強(qiáng)大,但是對(duì)于讀數(shù)據(jù)來(lái)說(shuō)卻很糟糕,這就是當(dāng)年他需要 CQRS 的原因:用多個(gè)模型來(lái)表示相同的數(shù)據(jù)。
CQRS 是如何實(shí)現(xiàn)這個(gè)目標(biāo)的?通過(guò)確保只有一個(gè)模型作為數(shù)據(jù)的源頭,所有的修改都通過(guò)這個(gè)模型來(lái)達(dá)到。
讓我們來(lái)看看這個(gè)解讀如何幫助我們解決一些復(fù)雜的問(wèn)題。
復(fù)雜性陷阱一:?jiǎn)蜗蛎?,或者說(shuō)過(guò)度的隔離
據(jù)我所知,所有的 CQRS 的定義都遵循這個(gè)模式。
- CQRS 是基于 CQS 原則,它指出,操作應(yīng)該被分為兩組:改變數(shù)據(jù)的命令和查詢(xún)數(shù)據(jù)的命令。一旦我們將這一原則提升到架構(gòu)層面,我們就會(huì)得到一個(gè)系統(tǒng),用例被隔離成相同的兩組:命令和查詢(xún)。每個(gè)用例既可以是命令,也可以是查詢(xún),但絕對(duì)不能同時(shí)是命令和查詢(xún)。
- 一旦用例被隔離,我們會(huì)得到很多好處:多種模型、不同的持久化機(jī)制、獨(dú)立的可擴(kuò)展性等。
你是否感覺(jué)到這里有什么問(wèn)題?這個(gè)問(wèn)題很微妙:所有的 CQRS 定義通常都是從解決方案 — 隔離開(kāi)始,之后才定義問(wèn)題 — 多模型。這就導(dǎo)致了對(duì)隔離太過(guò)熱衷:甚至將命令定義為單向的,操作服務(wù)器只返回 Ack/Nack 響應(yīng),必須輪詢(xún)一些讀模型存儲(chǔ)的實(shí)際命令才能返回結(jié)果。換句話說(shuō),復(fù)雜度如地獄式的釋放。
解決辦法:放寬隔離
讓我們退一步,重新考慮一下隔離的問(wèn)題。我們已經(jīng)看到,根據(jù) CQRS 的說(shuō)法,為了在多個(gè)模型中表示相同的數(shù)據(jù),一個(gè)用例既可以寫(xiě)數(shù)據(jù),也可以讀數(shù)據(jù)。讀取模型不應(yīng)該更新任何東西,這一點(diǎn)是不言而喻的,否則我們最終會(huì)有多個(gè)數(shù)據(jù)來(lái)源。但是,你真的應(yīng)該讓你的命令空轉(zhuǎn)嗎?
其實(shí)不然,在不違反任何原則的情況下,一個(gè)命令可以安全地返回以下數(shù)據(jù)。
- 執(zhí)行結(jié)果:成功/失敗。
- 如果失?。哄e(cuò)誤信息或驗(yàn)證錯(cuò)誤。
- 如果成功:聚合的新版本號(hào)。
這些信息將極大地改善你的系統(tǒng)的用戶(hù)體驗(yàn),因?yàn)椋?/p>
- 你不需要向外部來(lái)源查詢(xún)命令執(zhí)行結(jié)果,你馬上就能得到它。在驗(yàn)證命令,以及返回錯(cuò)誤信息方面變得非常簡(jiǎn)單;以及
- 如果你想刷新顯示的數(shù)據(jù),你可以使用聚合的新版本來(lái)確定視圖模型是否反映了已執(zhí)行的命令。不會(huì)再顯示陳舊的數(shù)據(jù)了。
說(shuō)到數(shù)據(jù),我們能不能再放寬一點(diǎn)隔離?在很多情況下,受影響的聚合內(nèi)部包含的任何數(shù)據(jù)都可以作為命令執(zhí)行結(jié)果的一部分返回。但是,這里有一點(diǎn)細(xì)微的差別:確保返回的數(shù)據(jù)可以在以后從其中一個(gè)讀取模型中查詢(xún)。否則,在響應(yīng)沒(méi)有到達(dá)客戶(hù)端的情況下,數(shù)據(jù)可能會(huì)有潛在的丟失風(fēng)險(xiǎn)。
你可以在 Daniel Whittaker 的博客 (4) 中看到這樣一個(gè)例子,他在博客中討論了命令執(zhí)行對(duì)象用于驗(yàn)證命令的使用。
另外,在這個(gè) gist 中 (5) ,你可以看到我在 C# 中使用的命令執(zhí)行結(jié)果對(duì)象。
復(fù)雜性陷阱二:Event sourcing (事件溯源)
由于歷史原因,CQRS 與 Event Sourcing 模式密切相關(guān)。畢竟,CQRS 的發(fā)明就是為了讓 Event Sourcing 模式成為可能。但是,讓我們重新評(píng)估一下這兩種模式之間的耦合關(guān)系。
正如我之前所說(shuō),CQRS 的目標(biāo)是允許在不同的模型中表示相同的數(shù)據(jù)。如果你正在使用 event source 域模型,你絕對(duì)需要 CQRS 來(lái)執(zhí)行查詢(xún)。然而,還有很多其他合理的理由來(lái)實(shí)現(xiàn) CQRS,這些理由與 event source 無(wú)關(guān)。
- 你的系統(tǒng)以不同的表示模型顯示其實(shí)體。
- 你必須支持不同的查詢(xún)模型(搜索、圖、文檔等)。
- 寫(xiě)入和讀入之間的差異很大,你希望將它們獨(dú)立擴(kuò)展。
- 你不喜歡 ORM。
這是否意味著在所有這些情況下,你必須走 Event Sourcing 路線?如果你這么做,你就深陷復(fù)雜度陷阱。Event source 是一種業(yè)務(wù)領(lǐng)域的建模方式,也可能是最復(fù)雜的方式。因此,只有當(dāng)你的業(yè)務(wù)領(lǐng)域證明你的業(yè)務(wù)領(lǐng)域是合理的,你才應(yīng)該采用 Event Sourcing。讓我們來(lái)看看如何在其他情況下實(shí)現(xiàn) CQRS。
解決方案:CQRS != Event Sourcing
我們已經(jīng)學(xué)會(huì)了通過(guò)編寫(xiě)事件處理程序來(lái)生成投影。如果沒(méi)有事件,如何實(shí)現(xiàn)投影?還有一種方法可以實(shí)現(xiàn)投影,我稱(chēng)之為“基于狀態(tài)的投影”。這個(gè)主題值得單獨(dú)寫(xiě)一個(gè)帖子,但我將簡(jiǎn)單介紹三種實(shí)現(xiàn)“基于狀態(tài)的投影”的方法。
1. "臟"標(biāo)志
你可以通過(guò)設(shè)置 IsDirty 標(biāo)志來(lái)標(biāo)記一個(gè)被更新的實(shí)體,并實(shí)現(xiàn)一個(gè)投影引擎來(lái)查詢(xún)臟實(shí)例,并將更新的數(shù)據(jù)投影到不同的模型中。要重建投影,你只需要將所有記錄的 dirty 標(biāo)志設(shè)置就可以了。
2. 追加
在關(guān)系型數(shù)據(jù)庫(kù)中,你可以在表層跟蹤提交。例如在 SQL Server 中,你有一個(gè)內(nèi)置的機(jī)制,即 "rowversion" 列。這樣的功能也可以在其他關(guān)系型數(shù)據(jù)庫(kù)中實(shí)現(xiàn)。投射引擎將以類(lèi)似于補(bǔ)訂的方式查詢(xún)更新的行,并將更新的數(shù)據(jù)進(jìn)行投影。要從頭開(kāi)始重建一個(gè)投影,必須將上次已知的提交 id “回滾” 到 0。
3. 數(shù)據(jù)庫(kù)視圖
如果你使用的是關(guān)系型數(shù)據(jù)庫(kù),而你需要的只是用不同的模型來(lái)表示它的數(shù)據(jù),那么數(shù)據(jù)庫(kù)視圖就很好用。沒(méi)錯(cuò),一個(gè)完全有效的 CQRS 系統(tǒng)可以在數(shù)據(jù)庫(kù)中實(shí)現(xiàn)。這可能是最不性感的解決方案 — 但它不僅可以工作,還自然地遵循了 CQRS 模式。
這些投影模型的方法可能并不酷,也不性感,但它們是有效的。我見(jiàn)過(guò)不少采用了這些方法的項(xiàng)目,它們的效果很好,沒(méi)有無(wú)端地淹沒(méi)在 event source 相關(guān)的復(fù)雜性中。
等等,我剛才是不是建議不惜一切代價(jià)忽略 Event Sourcing,因?yàn)樗軓?fù)雜?當(dāng)然不是! Event Sourcing 是你工具箱中最重要的工具之一。但是,作為任何工具,請(qǐng)?jiān)谄渖舷挛闹惺褂盟?— 能帶來(lái)商業(yè)價(jià)值的業(yè)務(wù)領(lǐng)域。核心子領(lǐng)域。另一方面,通用子域和支持子域,這些子域足夠簡(jiǎn)單,可以用事務(wù)腳本或活動(dòng)記錄模式實(shí)現(xiàn),但仍然可以從 CQRS 中受益。在這種情況下,使用最簡(jiǎn)單的工具來(lái)完成工作,并使用基于狀態(tài)的預(yù)測(cè)來(lái)獲取 CQRS 的好處。
復(fù)雜性陷阱三:好東西太多了
微服務(wù)的炒作吸引了很多人對(duì) CQRS 的關(guān)注:如果你有一組獨(dú)立的服務(wù)需要查詢(xún)彼此的數(shù)據(jù),那么 CQRS 就是通用的解決方案 (6) 。然而我已經(jīng)看到這種方法產(chǎn)生了巨大的數(shù)據(jù)流圖,在服務(wù)之間投射出大量的數(shù)據(jù)。
這不一定是壞事,但在很多情況下這可能是一個(gè)信號(hào),需要退一步重新考慮你的分解策略。有可能是你的服務(wù)過(guò)于細(xì)化,沒(méi)有反映出業(yè)務(wù)領(lǐng)域的邊界。如果是這種情況,你可以通過(guò)將服務(wù)邊界與相應(yīng)的業(yè)務(wù)域重新對(duì)齊,大大降低架構(gòu)的復(fù)雜性。
CQRS:解構(gòu)
我想用 CQRS 的圖來(lái)總結(jié)一下。

此圖與您在網(wǎng)上可以找到的其他圖表不同。
這就是我所看到的和實(shí)現(xiàn) CQRS 模式。命令有響應(yīng)。定義的投射機(jī)制是抽象的,與實(shí)現(xiàn)細(xì)節(jié)無(wú)關(guān)。里面可能是基于事件,或者是基于狀態(tài),甚至是數(shù)據(jù)庫(kù)視圖。最后,沒(méi)有事件源(Event Sourcing)。按照業(yè)務(wù)域的要求,對(duì)系統(tǒng)的業(yè)務(wù)邏輯進(jìn)行建模:活動(dòng)記錄、域模型或事件源域模型。
與每一個(gè)正確應(yīng)用的工具一樣,CQRS 應(yīng)該降低復(fù)雜性,而不是誘導(dǎo)復(fù)雜性。如果你的體系結(jié)構(gòu)的復(fù)雜性增加了,那么你很可能做錯(cuò)了。
文中相關(guān)鏈接:
http://codebetter.com/gregyoung/2010/02/20/cqrs-and-cap-theorem/
https://martinfowler.com/bliki/CQRS.html
https://youtu.be/LDW0QWie21s?t=448
http://danielwhittaker.me/2016/04/20/how-to-validate-commands-in-a-cqrs-application/
https://gist.github.com/vladikk/86da55d0eb09d7a291b9f9a5b406f2c9
https://www.ibm.com/developerworks/cloud/library/cl-build-app-using-microservices-and-cqrs-trs/
英文原文:
https://vladikk.com/2017/03/20/tackling-complexity-in-cqrs/
本文轉(zhuǎn)載自微信公眾號(hào)「高可用架構(gòu) 」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系高可用架構(gòu) 公眾號(hào)。

























