DDD 中的那些模式 — CQRS

JoshuaJin發表於2020-03-24

DDD 作為一種系統分析的方法論,最大的問題是如何在專案中實踐。而在實踐過程中必然會面臨許多的問題,「模式」是系統架構領域中一種常見的手段,能夠幫助開發人員與架構師在遭遇某種較為棘手,或是陌生的問題時,參考已有的成熟經驗與解決方案,從而優雅的解決自己專案中的問題。

從本期開始,我會開始介紹 DDD 中一些常見的模式,包括這些模式的背景,作用,優缺點,以及在使用過程中需要注意的地方。而本次的主角就是 CQRS,中文名為命令查詢職責分離。

為何要使用?

毋庸置疑「領域」在 DDD 中佔據了核心的地位,DDD 通過領域物件之間的互動實現業務邏輯與流程,並通過分層的方式將業務邏輯剝離出來,單獨進行維護,從而控制業務本身的複雜度。

但是作為一個業務系統,「查詢」的相關功能也是不可或缺的。在實現各式各樣的查詢功能時,往往會發現很難用領域模型來實現。假設在使用者需要一個訂單相關資訊的查詢功能,展現的是查詢結果的列表。列表中的資料來自於「訂單」,「商品」,「品類」,「送貨地址」等多個領域物件中的某幾個欄位。這樣的場景如果還是通過領域物件來封裝就顯的很麻煩,其次與領域知識也沒有太緊密的關係。

此時 CQRS 作為一種模式可以很好的解決以上的問題,那麼具體什麼是 CQRS 呢?又如何實現呢?

什麼是 CQRS?

CQRS — Command Query Responsibility Segregation,故名思義是將 command 與 query 分離的一種模式。query 很好理解,就是我們之前提到的「查詢」,那麼 command 即命令又是什麼呢?

CQRS 將系統中的操作分為兩類,即「命令」(Command) 與「查詢」(Query)。命令則是對會引起資料發生變化操作的總稱,即我們常說的新增,更新,刪除這些操作,都是命令。而查詢則和字面意思一樣,即不會對資料產生變化的操作,只是按照某些條件查詢資料。

CQRS 的核心思想是將這兩類不同的操作進行分離,然後在兩個獨立的「服務」中實現。這裡的「服務」一般是指兩個獨立部署的應用。在某些特殊情況下,也可以部署在同一個應用內的不同介面上。

Command 與 Query 對應的資料來源也應該是互相獨立的,即更新操作在一個資料來源,而查詢操作在另一個資料來源上。看到這裡,你可能想到一個問題,既然資料來源進行了分離,如何做到資料之間的同步呢?讓我們接著往下看。

實現 CQRS

讓我們先看一下 CQRS 的架構圖:

DDD 中的那些模式 — CQRS

從圖上可以看到,當 command 系統完成資料更新的操作後,會通過「領域事件」的方式通知 query 系統。query 系統在接受到事件之後更新自己的資料來源。所有的查詢操作都通過 query 系統暴露的介面完成。

從架構圖上來看,CQRS 的實現似乎並不難,許多開發者覺得無非是「增刪改」一套系統一個資料庫,「查詢」一個系統一個資料庫而已,有點類似「讀寫分離」,並沒有什麼特別的地方。但是真正要使用 CQRS 是有許多問題與細節要解決的。

CQRS 帶來的問題

事務

其實仔細的思考一下,你應該很快會發現 CQRS 需要面臨的一個最大的問題: 事務。在原本單一程式,單一資料來源的系統中,依靠關係型資料庫的事務特效能夠很好的保證資料的完整性。但是在 CQRS 中這一切都發生了變化。

當 command 端完成資料更新後,需要通過事件的形式通知 query 端系統,這就存在著一定的時間差,如果你的業務對於資料完整的實時性非常高,那麼可能 CQRS 不一定適合你。

其次一個 command 觸發的事件在 query 端可能需要更新數個資料模型,而這也是有可能失敗的。一旦更新失敗那麼資料就會長時間的處於不一致狀態,需要外部的介入。這也是在使用 CQRS 之前就需要考慮的。

從事務的角度來看 CQRS,你需要面對的是問題從根本來說是個最終一致性的問題,所以如果你的團隊在這塊沒有太多經驗的話,那麼需要提前學習並積累一定的經驗。

基礎設施與技術能力的挑戰

CQRS的另一個問題是沒有一個成熟易用的框架,Axon 可能算一個,但是 Axon 本身是一個重量級且依賴性較高的框架。為了 CQRS 而引入 Axon 有點捨本逐末的意思,因此大部分時間你不得不自己動手實現 CQRS。

一個成熟可靠的 CQRS 系統對於基礎設施有一定的要求,例如為了實現領域事件,一個可靠的訊息中介軟體是不可或缺的。不然頻繁丟失事件造成資料不一致的情況會讓運維人員焦頭爛額。之前提到的分散式事務與最終一致性的問題也需要專門的中介軟體或是框架的支援,這些不僅僅提升了對基礎設施的要求,對於開發,運維也提出了更高的要求。

開發過程中需要加入對於事件的支援,系統設計的思路也同樣需要一定的轉變。在定義 command 時需要設計對應的事件,設計事件的型別與資料結構,所以在這方面也對開發團隊提出了新的要求。

因此在開始使用 CQRS 之前不妨對自己團隊的基礎設施以及開發能力做一次全面的評估,儘早的識別出短板,並進行有目的的改進與強化,避免在開發過程中別某些問題卡住。

查詢模型的設計

雖然 CQRS 為我們分離了領域模型和服務於查詢功能的資料模型,但這意味著我們需要設計另一套針對查詢功能的資料模型。一般比較簡單的做法是按照查詢功能所需的資料進行設計,即針對每一個查詢介面設計一個資料檢視,當收到領域事件時更新有關聯的資料檢視。

但是這種簡單做法帶來的問題就是當查詢介面越來越多時就會難以管理,仍然需要按照 DDD 中劃分 BC 的思路將屬於一個 BC 的查詢集中管理作為整個查詢系統的一個上下文,或是乾脆獨立出來做一個微服務。所以即使引入了 CQRS,我們依然需要使用領域驅動的思路設計查詢介面。

與 Event Sourcing 的關係

Event Sourcing是由 Martin Fowler 提出的一個企業架構模式。簡單的來說它會將系統所有產生業務行為以 append-only 的形式儲存起來,通俗的說就是「流水賬」。它的優點是可以「回溯」,因為記錄了每一次資料變動的資訊,所以當出現 bug 或是需要排查業務資料問題時就非常的方便。但是它的缺點同樣明顯,就是當需要查詢最新狀態的資料時需要做大量的計算,例如賬戶餘額這樣的資料。

許多討論 CQRS 的文章中都會談及 Event Sourcing,認為這是兩個需要配套使用的模式。但是從我實際使用的角度而言,這兩個模式其實並沒有什麼必然的聯絡。Command 端只需要關心領域模型的更新成功與否,同時使用 Aggregate 這樣的領域物件保證資料的完整性,而 Query 端關心的是接收到領域事件後更新對應的資料模型,對於「回溯」這樣的特性並沒有強制的要求。的確 Event Sourcing 可以幫助我們構建更為穩定,功能更為強大的 CQRS 系統,但是 Event Sourcing 本身的複雜性可能比 CQRS 有過之而無不及,所以在沒有特殊需要的情況下,CQRS 與 Event Sourcing 不需要綁在一起。

不同型別的資料儲存引擎

這一點其實不能算是問題,更多的是一項挑戰或是優勢。由於分離了領域模型與資料模型,因此意味著我們可以在 Query 端使用與查詢需求更為貼近的資料儲存引擎,例如 NoSQL,ElasticSearch 等。

比較常見的情況是 Command 端依然使用傳統的關係型資料庫,但是對於那些比較特殊的查詢則使用專門的資料儲存。例如在一些基於關鍵字進行全文檢索的場景,如果依然使用關係型資料庫,通過 like 這樣的 SQL 查詢,很容易遇到效能問題。此時則可以將資料儲存換為 ElasticSearch 這樣的檢索引擎,通過反向索引提取關鍵字查詢,在效能方面會得到非常明顯的提升。在另一些需要非結構化資料查詢的場景,Json 是一種不錯的儲存格式,雖然現在比較新版本的關係型資料庫都提供了 Json 格式的儲存與查詢,但是 MongoDB 這樣的文件型資料庫顯得更為簡單高效,此時 Query 端靈活的優勢就更為明顯。

小結

CQRS 在 DDD 中是一種常常被提及的模式,它的用途在於將領域模型與查詢功能進行分離,讓一些複雜的查詢擺脫領域模型的限制,以更為簡單的 DTO 形式展現查詢結果。同時分離了不同的資料儲存結構,讓開發者按照查詢的功能與要求更加自由的選擇資料儲存引擎。

同樣的,CQRS 在帶來架構自由與便利的同時也不可避免的引入了額外的複雜性與技能要求,例如對於分散式事務,訊息中介軟體的管理,資料模型的設計等等,所以在引入 CQRS 之前需要對團隊能力與現有架構做仔細的分析,對短板進行必要的提升。如果現有系統邏輯較為簡單,只是一些 CRUD,那麼並不建議使用 CQRS。但是如果你的業務系統已經非常龐大,業務流程龐雜,邏輯繁瑣,那麼不妨嘗試使用 CQRS 將 Command 與 Query 進行拆分,將領域模型與資料模型的邊界劃分的更清晰些。

歡迎關注我的微訊號「且把金針度與人」,獲取更多高質量文章

DDD 中的那些模式 — CQRS

相關文章