企業及應用架構X

CQRS

Posted by BY Skyleaf on March 23, 2019

前言

這篇稍微紀錄一下這次讀本書第十篇的感想,這篇介紹CQRS的紀錄,所謂的讀寫分離,我看DDD的目的之一就是為了CQRS。

图

Agenda

  • 查詢線
  • 命令線

查詢線

  • 查詢: “查詢不以任何方式修改系統狀態,只返回數據”

在這之前先講一下CQRS,為什麼會需要它,當你看完DDD後你一定會想試試看,然後你就會遇到複雜性的問題,你的Module越來越多,他要包含讀,也要包含寫。越來越複雜越來越難維護,我們看架構為的就是日後的維護。CQRS會需要兩個不同的領域層而不是一個,本文想說的是如果你讀取資料沒什麼邏輯處理,你真的還需要從ControllerService –> Domain –> Repository –> DB 這樣的流程嗎? 還是可以簡化一點讀取流程? 答案是當然可以簡化,走這麼長是沒必要的。讀跟寫拆成兩個不同的模式,各自有各自成長的方式,讀可以搭配NoSQL,寫可以搭配Queue跟事件。優點可以說是降低複雜性跟提高可升縮性兩點。在讀取方面會建議使用多一個DB或是NoSQL,隔離讀寫需要。

在DDD使用單一模型的方式,會遇到無法共用,或是共用的協調上較於複雜的狀況,使用CQRS就是可以解套它。一個單一個模型它可能再查詢的時候需要get屬性,把set改成private,但由於是共用,在命令的使用上你還是會有changeXXX屬性的時候,RD就可以去調用它,也就有機會更改屬性,雖然讀取的時候開發人員不會這樣做,但總是有個窗口讓他可以這樣做(莫非定律),另一個論點是統一語言,在做查詢的模型並不是只是為了查詢,他也包含命令,這樣違背統一語言了。

使用CQRS解套,在查詢方面,你會使用到貧血模型,沒錯它在DDD不好,但在CQRS是好的。在MVC你的ViewModel可能就是你的查詢模型的DTO。另外注意本文說到 “聚合的概念在只讀模式理不再重要”。這樣說是因為聚合,你會有聚合根跟它的上下文entity,在命令線你會需要這樣的組合作商業邏輯處理,但在查詢線上,你就只是需要顯示數據,哪有什麼上下文邏輯要處理,當然你可以說不是取出來的數據都是我要顯示的,但這裡想說的是不要被DDD內的設計限制了你的CQRS解套複雜性的方式,讀跟寫的模式是可以分開設計的。

  • 取出的數據如果要做一些些邏輯上的處理可以使用Layer Expression Tree (LET),它的概念是使用IQuerable或是IEnerable方式,多半數據要處理的邏輯會跟 **過濾數據** 和 **組合數據** 和 **判別數據** 等等有關。大部分我在取得該查詢線的時候,複雜一點的資料量在DB方面我使用StoreProcedure,先行過濾不需要的資料,但不做資料 **邏輯** 處理,邏輯方面我把它擺在Application(應用程序層)內,使用Linq語法處理,過程盡量是延遲載入的狀態,說是"盡量"是因為有些複雜的查詢在處理邏輯上需要重新編排的時候會需要實體化ToList()。但能不實體就不實體。最後在投影數據給ViewModel或是xxxReponse物件。LET的優點就是你不需要DTO在每層趴趴走,你的ViewModel就解決了。
  • 本文這裡也有說到把需要的資料在一開始就存到內存呢? 答案是可以的,但是我覺得要適量,過多的資料在內存會導致你的開發下降,試問你開發的時候就將所有資料量往你的本機電腦內存裡丟,但我只是想開發個小功能,丟全部資料是何苦呢!! 建議作法是採用統一快取系統,類似Redis。特定需要高效能的在往MemoryCache丟。

CQRS

命令線

  • 命令: “修改系統的狀態,但不返回數據”

命令線的特性就是他會改變系統狀態。大部分使用的溝通是透過Message的概念方式,使用者request一個命令,傳到系統做後續處理,可以是異步的訂閱式,可以是直接處理訊息內容的方式。這裡想介紹的是命令式的消息可以分兩種: 事件跟命令。

  • 命令: 他像是一種命令消息會執行某種任務提交給系統
  • 事件: 它是一種通知已發生的消息,告知有訂閱的事件的可以執行某種行為。它可以解決系統的效能跟領域層級的複雜邏輯。

命令會有自己的命令總線而事件會有自己的事件總線來管理裡面的訊息,這讓我想到Queue。總線就是Broker。這裡還必須提到Saga,它是一種處理命令或是事件流程的統稱。假如我收到一個更新商品A的訊息,這段訊息會寫入命令總線,處理這段命令的Saga會去擷取這段訊息,然後作處理,它也可能在觸發另一個命令訊息或是事件訊息,例如商品A資料更新完畢,發送更新線上Cache資料訊息,發送壓圖需球事件等等。有發現嗎?擴充是不是很方便,你只要建立好要做的Saga,讓他去註冊到總線即可。在要觸發它的地方加入觸發即可,延展性提升了。命令總線解偶了應用程序層跟領域服務之間的關聯性。以往會寫成一大包,統一做完,但這樣效能差,維護上也麻煩。最後注意如果要異步處理請選擇 事件不是 命令

使用這種異步模式最困難的在於失敗的時候,或是過程需要中斷它的時候,要保持最終一致性。在很多WebAPI的設計我喜歡Restful的,不管重複request相同的命令,拿到的結果都是一樣的,由於這樣我知道在某種層度上如果過程有失敗,肯定是源頭給的資料不正確,資料源不正確導致後續流程失敗是可預期的狀況結果,它也應該是這樣,只有在你給正確的資料源才會正常運作。例如你給不對的帳密一樣,他一定不會給你會員權限跟會員資料的,過程會出錯,也不會走完。

結論

  • 這裡要說明DDD的統一語言跟綁定上下文的方式還是非常推薦的,不是CQRS就直接否定DDD,你可能會看到很多原本在DDD不建議的事項在CQRS卻可以,因為CQRS只是一種模式,而這個模式可以簡化DDD的一個複雜度跟延展性。

來源

  1. Microsoft.NET企業級應用架構設計
  2. eShopOnContainers 訂購微服務的領域模型結構
  3. NAA4E
  4. 微軟官方
  5. CQRS
  6. 浅谈命令查询职责分离(CQRS)模式