最全面的CQRS和事件溯源介紹 - Software House ASC

banq發表於2019-05-23

CQRS(Command-Query Responsibility Segregation) 是一種模式,它告訴我們將資料的查詢與資料的操作分開。

它源於Bertrand Mayer設計的命令查詢分離(CQS)原理。CQS宣告一個類只能有兩種方法:改變狀態並返回void的方法和返回狀態但不改變它的方法。

Greg Young 是負責命名這種模式為CQRS 並推廣它的人。如果您在網際網路上搜尋CQRS,您會發現許多由Greg製作的優秀帖子和視訊。例如,你可以找到在CQRS模式的優秀和非常簡單的解釋在這個帖子。

我們想要展示保險領域的例子 - PolicyService。負責管理保險單的服務。以下是在應用CQRS之前具有介面的程式碼段。所有方法(寫入和讀取)都在一個類中。

interface PolicyService {
    void ConvertOfferToPolicy(ConvertOfferRequest convertReq);
    PolicyDetailsDto GetPolicy(long id);
    void AnnexPolicy(AnnexRequestDto annexReq);
    List SearchPolicies(PolicySearchFilter filter);
    void TerminatePolicy(TerminatePolicyRequest terminateReq);
    void ChangePayer(ChangePayerRequest req);
    List FindPoliciesToRenew(RenewFilter filter);
}

如果我們在這種情況下使用CQRS模式,我們會得到兩個獨立的類,更好地滿足SRP原則。

interface PolicyComandService {
    void ConvertOfferToPolicy(ConvertOfferRequest convertReq);
    void AnnexPolicy(AnnexRequestDto annexReq);
    void TerminatePolicy(TerminatePolicyRequest terminateReq);
    void ChangePayer(ChangePayerRequest req);
}

interface PolicyQueryService {
    PolicyDetailsDto GetPolicy(long id);
    List SearchPolicies(PolicySearchFilter filter);
    List FindPoliciesToRenew(RenewFilter filter);  
}

這是應用CQRS的第一步。什麼是簡單的轉變會帶來很大的後果並開闢新的可能性,我們將在本文的後面部分進行探討。

CQRS能做什麼?

大多數時候,改變狀態所需的資料在形式或數量上都不同於使用者需要查詢所需的資料。使用相同的模型來一起處理查詢和命令會會導致模型膨脹,只依靠一種型別來操作所需的所有東西,模型複雜性也會增加,聚合大小通常會更大。

CQRS使我們能夠使用不同的模型來改變狀態和不同的模型來支援查詢。通常寫操作的頻率低於讀操作。 具有單獨的模型和分離的資料庫引擎允許我們獨立地擴充套件查詢端並更好地處理併發訪問,因為讀取端不再堵塞寫入或命令端(在相反的情況下)。

使用單獨的命令和查詢模型,我們可以將這些職責分配給具有不同技能的不同團隊。例如,您可以為高技能的OOP開發人員分配命令端,而熟悉SQL開發人員可以實現查詢端。CQRS讓您擴充套件您的團隊,讓您最好的開發人員專注於核心的東西。

CQRS是一個架構嗎?

人們常常弄錯了。CQRS不是頂級/系統級架構。架構的示例包括:分層埠和介面卡(六角形或六邊形架構)。CQRS是您在服務/應用程式“內部”應用的模式,您只能將其應用於您的部分服務。(banq注:CQRS是一種服務模型,微服務的模型,也就是指導你怎麼做微服務的)

實施示例 

有許多方法可以實現CQRS。它們具有不同的後果,這種解決方案的複雜性和適用性取決於您的系統環境。如果您是擁有1.5億使用者的Netflix,您需要採用不同的方法,並且不同的解決方案適用於僅有數百名使用者的典型企業應用。我們認為,特別是在處理現有(遺留)專案時,最好的方法是解決CQRS的演變問題。

我們從不使用CQRS的解決方案開始。

最全面的CQRS和事件溯源介紹 - Software House ASC

UI(通過控制器層)使用服務外觀層,該層負責協調域模型執行的業務操作。模型儲存在關聯式資料庫中。在我們的示例中,有PolicyService類(JavaC#),它負責處理與策略相關的所有業務方法。

我們使用一個模型進行讀寫。執行業務操作時,我們使用搜尋功能也是通過相同類實現,這可能會導致您的域模型只具有搜尋所需的屬性,或者更糟糕的是,您可能會強制設計域模型以便更輕鬆地查詢它。

在這個例子中,我們想要顯示開發人員使用分離模型進行寫入側和讀取側的程式碼。

最全面的CQRS和事件溯源介紹 - Software House ASC

在該示例中,使用中介者模式如XXXHandler,中介的作用是確保將命令或查詢傳遞給其處理程式。中介接收命令/查詢,該命令/查詢只不過是描述意圖的訊息,並將其傳遞給處理程式,然後處理程式負責呼叫領域模型執行預期的行為。因此,可以將此過程視為對服務層的呼叫 - 匯流排在其間進行訊息的管道連線。在Java示例中,我們建立了Bus類,它是此模式的實現。Registry負責將處理程式與命令/查詢相關聯。在C#示例中,我們使用MediatR庫為我們完成所有這些。您可以在我們的另一篇文章中閱讀有關MediatR的更多資訊。

現在我們有了單獨的命令和查詢入口點,我們可以引入不同的模型來處理它。NoCQRS解決方案使用RDBMS和ORM - 企業應用程式中的典型堆疊。通過此改變,我們可以將域模型用作命令模型。這個模型得到了簡化:一些關聯僅用於不再需要的讀取查詢,一些欄位不再需要。

在查詢模型上,我們可以在資料庫中定義檢視並使用ORM對映它,或者,對於查詢模型,我們可以停止使用重量級ORM並將其替換為普通的舊JDBC模板或Java中的JOOQ或在.NET中的Dapper

如果要避免在資料庫中定義檢視的複雜查詢,可以執行下一步,並使用旨在處理查詢的表替換檢視。這些表將具有簡單的結構,資料對映為使用者在螢幕上看到的內容以及使用者需要搜尋的內容。(banq注:專門為查詢讀取設計的資料表結構)。新增這種型別的表替代資料庫檢視消除了編寫複雜查詢的負擔,併為擴充套件解決方案開闢了新的可能性,但它要求您以某種方式使您的域命令模型與查詢模型表保持“同步”。

同步方式:

  • 使用Spring中的應用程式事件(示例)或使用域事件在同一事務中同步
  • 在命令處理程式中的同一事務中同步,
  • 非同步使用某種記憶體事件匯流排,導致最終的一致性,
  • 非同步使用像RabbitMQ這樣的某種佇列中介軟體,從而最終實現一致性。

最佳實踐:

  • 您應該為每個螢幕/視窗小部件(螢幕片段)構建一個表/檢視。
  • 表之間的關係應該是螢幕元素之間關係的模型。
  • “檢視錶格”包含螢幕上顯示的每個欄位的列。
  • 讀取模型不應該進行任何計算,而是在命令模型中計算資料並更新讀取模型。
  • 讀模型應儲存預先計算的資料。
  • 最後但同樣重要的是:不要害怕重複。

命令模型和查詢模型之間同步方法的選擇取決於許多標準。即使使用資料庫檢視,您也可以獲得很好的結果,因為您可以使用只讀副本來擴充套件資料庫,該副本僅用於查詢您建立的檢視。

具有單獨的表簡化了讀取,因為您不必再​​編寫複雜的SQL,但您必須自己編寫用於更新查詢模型的程式碼。

沒有神奇的框架會為你做這件事。與給定命令模型部件相關的讀取模型的數量也是決策因素。如果您有一個聚合的2-3個查詢模型,您可以安全地呼叫命令處理程式中的所有更新程式。它不會影響效能,但是如果你有10個,那麼你可以考慮在更新聚合的事務之外非同步執行它。在這種情況下,您必須檢查是否允許最終一致性。這比業務決策更具商業決策,必須與業務使用者討論。

擁有單獨的查詢表是將CQRS解決方案提升到新水平的一個很好的步驟。

如果您想了解更多資訊,請檢視我們的示例,使用JavaC#

單獨的儲存引擎

在這種方法中,我們為查詢模型和命令模型使用不同的儲存引擎,例如:

  • ElasticSearch用於查詢端,JPA用於命令端,
  • ElasticSearch用於查詢端,DocumentDb用於命令端,
  • 用於查詢的DocumentDb,在命令端的RDBMS中將聚合儲存為JSON。

每個命令處理程式都應該發出包含所發生事件 ,領域事件Event是一個命名物件,表示在指定物件中發生的某些更改。事件應提供有關在業務操作期間更改的資料的資訊。事件是域的一部分。在我們的示例中,我們有一些關於保險政策的事件 - PolicyCreated,PolicyAnnexed,PolicyTerminated,PolicyAnnexCancelledJava示例C#示例)。

在讀取方面,我們建立了事件處理程式(方法在特定型別的事件進入時執行),它們負責事件的投影建立(banq注:把事件再執行一遍更改查詢資料表,此為事件的投影)。這些事件處理程式對永續性讀取模型(Java示例C#示例)執行CRUD操作。

什麼是投影?投影是將事件流轉換(或聚合)為資料表結構或資料庫檢視的過程。投影是將事件流轉換(或彙總)為結構表示。這可以稱為許多名稱:永續性讀取模型,查詢模型或檢視。

通過這種方法,我們可以應用不同的工具來執行查詢,並使用不同的工具來執 通過這種方式,我們可以實現更好的效能和可伸縮性,但卻以複雜性為代價。在典型的業務系統中,系統中執行的絕大多數操作將使用讀取側/查詢模型。該元素應該為更高的負載做好準備,它應該是可擴充套件的,並允許構建允許高階搜尋的複雜查詢。使用這種方法,我們將不得不處理最終的一致性,因為各種資料來源之間的分散式事務是效能殺手,而大多數NoSQL資料庫都不支援它。

CQRS與事件採購(CQRS-ES)

下一步是更改命令端以使用事件源。這個版本的架構非常類似於上面(當我們使用單獨的儲存引擎時)。

最全面的CQRS和事件溯源介紹 - Software House ASC

關鍵區別在於命令模型。我們使用Event Store作為持久儲存,而不是RDBMS和ORM。我們不儲存實際的物件狀態,而是儲存事件流。這種管理狀態的模式被命名為Event Sourcing 

我們不是通過改變先前的狀態來保持系統的當前狀態,而是將事件(變化)附加到過去事件(變化)的順序列表中。這樣我們不僅可以瞭解系統的當前狀態,還可以輕鬆跟蹤我們是如何達到這種狀態的。

下面的示例顯示了基於足球遊戲比賽域的不同狀態管理方法。

最全面的CQRS和事件溯源介紹 - Software House ASC上圖顯示了Game物件的傳統狀態管理。我們有關於比賽結果以及比賽開始/結束的資訊。當然,我們可以在這裡建模其他資訊,例如得分目標列表,犯規犯規列表,角落列表。但是,您必須承認 - 足球比賽的領域理想地由一系列隨時間發生的事件描述。

最全面的CQRS和事件溯源介紹 - Software House ASC當使用Event Sourcing來管理Game物件的狀態時,我們可以準確地重現整個比賽。我們有關於哪些事件影響了當前物件狀態的資訊。上圖顯示每個事件都反映在特定的類中。這就是Event Sourcing的神奇之處。

大多數文章中提到的主要事件溯源優勢之一是您不會丟失任何資訊。在傳統模型中,每次更新都會刪除以前的狀態 之前的狀態丟失了。您可以說,有像Envers這樣的日誌,備份和庫,但它們並沒有為您提供有關更改原因的明確資訊。它們只顯示資料已更改的內容,而不是原因。在事件源方法中,您可以在域中的業務事件之後為事件建模,因此它不僅顯示資料更改,還顯示更改原因。

下一個優點是,通過一系列事件儲存域聚合可以極大地簡化永續性模型。您不再需要設計表格和它之間的關係。您不再受ORM可以和不能對映的限制。在使用像Hibernate這樣非常先進的解決方案時,我們發現了一些情況,當我們不得不從我們域中的某些設計概念中辭職時,因為很難或不可能對映到資料庫。

有越來越多的解決方案支援使用Event Sourcing(EventStoreStreamstoneMartenAxonEventuate)建立應用程式。在我們的示例中,我們使用從Greg Young的示例派生的記憶體事件儲存(Java示例C#示例)的自己實現。這不是生產就緒的實現。對於生產級解決方案,您應該應用更復雜的解決方案,如EventStoreAxon

哪些系統值得使用事件採購?

  • 你的系統有許多不是普通CRUD的行為,
  • 重建物件的歷史狀態非常重要,
  • 商業使用者看到擁有統計,機器學習或其他目的的完整歷史的優勢,
  • 您的領域最好由事件描述(例如,跟蹤輔助車輛活動的應用程式 banq注:物聯網等跟蹤系統,跟蹤錢流,跟蹤物流,跟蹤資訊流)。

我應該使用CQRS / ES框架嗎?

如果您對CQRS / ES沒有經驗,則不應該從任何框架開始。從核心域開始,實現一些業務功能。當您的業務開始工作時,請關注技術內容。在開始實現自己的事件儲存或命令匯流排之前,請評估Event Store或Axon等可用選項。有很多事情需要考慮,還有許多陷阱(併發,錯誤處理,版本控制,模式遷移)。

總結

有兩個陣營:一個說你應該總是使用CQRS / ES,另一個說你應該只使用你的解決方案的一部分,並且只有當你需要具有高效能/可用性/可擴充套件性系統的高度併發系統時。您應該始終根據您的要求評估您的選擇。

即使是最簡單的CQRS形式也能在不增加複雜性的情況下為您提供良好的結果。例如,使用檢視進行搜尋而不是使用域模型可以簡化事情。在我們的系統中,我們還發現很多地方新增專門的讀取模型表並同步更新它們給了我們非常好的結果(比如擺脫20多個表連線4個聯合的檢視定義並用一個表替換它)。  只要允許最終的一致性,使用像ElasticSearch這樣的專用搜尋引擎也是一個安全的選擇。

如果您選擇使用不同的儲存引擎,事件匯流排和其他技術元件,CQRS可能會產生非常複雜的技術解決方案。只有一些複雜的場景和可擴充套件性要求才能證明這種複雜性(如果你在Netflix規模上執行)。同時,您還可以使用簡單的技術解決方案應用CQRS,並從此模式中受益 - 您不需要Kafka來執行CQRS。

我們為這篇部落格文章準備了兩個版本的demo,一個用於Java開發人員,第二個用於.NET開發人員。以下連結:

CQRS的利弊​​​​​​​

優點:

  • 更好的系統效能和可擴充套件性,
  • 更好的併發訪問處理,
  • 更好的團隊可擴充套件性,
  • 不太複雜的域模型和簡單的查詢模型。

缺點:

  • 讀寫模型必須保持同步,
  • 如果您選擇兩個不同的引擎進行讀取和寫入,維護和管理成本,
  • 最終的一致性並不總是允許的。

ES事件溯源利弊

優點:

  • 僅附加模型非常適合效能,可擴充套件性
  • 沒有死鎖
  • 事件(事實)被商業專家很好地理解,一些領域本質上是事件來源:會計,醫療保健,交易
  • 審計跟蹤免費
  • 我們可以在任何時間點獲得物件狀態
  • 易於測試和除錯
  • 資料模型與域模型分離
  • 無阻抗不匹配(物件模型與資料模型)
  • 靈活性 - 可以從相同的事件流構建許多不同的域模型
  • 我們可以將此模型用於逆轉事件,追溯事件
  • 沒有更多的ORM - 由於我們的物件是根據事件構建的,我們不必在關聯式資料庫中反映它

缺點:

  • 開發人員管理狀態和構建聚合不是很自然的方式,需要時間來習慣
  • 查詢超出一個聚合更難(您必須為要新增到系統的每種型別的查詢構建投影),
  • 事件模式更改比關係模型(缺少標準模式遷移工具)困難得多
  • 你必須從一開始就考慮版本控制處理。

​​​​​​​

 

相關文章