天天看點

DDD 中的那些模式 — CQRS

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

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

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

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

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

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

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

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

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

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

DDD 中的那些模式 — CQRS

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

從架構圖上來看,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是由 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 進行拆分,将領域模型與資料模型的邊界劃分的更清晰些。