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

19-05-23 banq
                   

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的解決方案開始。

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

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

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

在該示例中,使用中介者模式如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)

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

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

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

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

上圖顯示了Game對象的傳統狀態管理。我們有關于比賽結果以及比賽開始/結束的信息。當然,我們可以在這里建模其他信息,例如得分目標列表,犯規犯規列表,角落列表。但是,您必須承認 - 足球比賽的領域理想地由一系列隨時間發生的事件描述。

當使用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 - 由于我們的對象是根據事件構建的,我們不必在關系數據庫中反映它

缺點:

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

???????

?

                   

4
美女漫画大全