使用Redis/RabbitMQ/EventStore實現事件溯源CQRS微服務應用 - Aram Koukia

banq發表於2019-07-04

這是一篇EventSourcing/CQRS實現的教程文章,從原理模式到具體技術產品選型都闡述得比較詳細。以下是架構圖:


使用Redis/RabbitMQ/EventStore實現事件溯源CQRS微服務應用 - Aram Koukia
這裡簡單介紹一下這個架構的工作原理:

  1. UI應用程式(例如Angular應用程式)透過Http向API閘道器發出請求。
  2. 該請求是“命令”或“查詢”。
  3. 命令將傳遞給Domain Microservice,它只接受命令。Domain Microservice根本不接受查詢。
  4. Domain Microservice接收命令,將事件保留在記憶體中,並將事件傳送到Event Store資料庫。
  5. Domain Microservice還會將這些事件釋出到Service Bus。
  6. 由於Read Model Microservice對此事件感興趣,因此它將被訂閱此事件,因此它將透過服務匯流排接收此事件,並更新其非規範化資料的版本並準備好進行查詢。
  7. 如果請求是“查詢”,它將被髮送到讀取模型微服務

Domain Service裡面的細節:

使用Redis/RabbitMQ/EventStore實現事件溯源CQRS微服務應用 - Aram Koukia
以下是Domain Microservice中的簡單事件流:

  1. Web API Controller僅接受命令,當它獲得命令時,它只是轉發給命令處理程式。
  2. 然後,Command Handler會將命令傳送到Aggregate以執行與該命令相關的業務邏輯。
  3. 完成後,將通知命令處理程式聚合操作已完成,並且已準備好保留事件。
  4. 然後,事件將透過儲存庫持久儲存到Event Store,同樣的事件也將釋出到Service Bus以供其他服務使用。

兩階段提交
這裡我們可以將訊息保留到事件儲存但訊息匯流排訊息釋出失敗,現在,我們需要做的就是識別失敗,然後返回事件儲存並重放將釋出訊息的訊息.

設計模式和元件
以下模式和元件僅支援分散式和鬆散耦合系統的思想。


使用CQRS Pattern,我們的想法是將查詢和命令視為兩個獨立的問題來處理,因此在我們的程式碼中,我們將查詢和命令傳送到不同的路徑來處理它們。
僅憑這個定義,如果你在一個服務中同時擁有命令和查詢,它對我們的微服務實現沒有任何幫助,因為它是服務中發生的內部,但是如果我們想要分離命令和查詢兩個不同的微服務,一個服務處理查詢,另一個處理命令,然後如果我們使用這個模式,分離將非常簡單,因為理論上查詢和命令應該有單獨的程式碼路徑。
  • 事件溯源模式

使用Event Sourcing Pattern,我們的想法是不在資料儲存中儲存物件的最終狀態,而是使用這種“僅不斷追加事件”的方法,我們只將事件記錄為事件流中的序列,每次我們想知道物件的當前狀態是什麼,我們從事件流的開頭開始,重新播放執行它們,並找出最終狀態。
老實說,這種模式是一種很好的模式,在構建微服務時很有用,但我仍然不完全理解,這有助於我們構建分散式解耦應用程式。
有人建議,我們將這些事件放在Event Store中,如果其他微服務需要這些物件的狀態,他們可以檢視這些事件流並找出物件的狀態,而無需轉到得到它的原始服務。這對我來說又是另一個單點故障,在Monolit單點系統中,我們都是這樣查詢共享表來找出狀態的,而現在我們都查詢事件儲存庫?
無論如何,我認為在這一點上我認為寧願使用服務匯流排(接下來解釋)並讓每個服務訪問他們自己的資料儲存中所需的資料(即使是重複資料)。

Service Bus是Microservices實現中非常重要的一部分,這是我們將使用Publisher和Subscriber模型在Microservices之間進行非同步通訊的工具。使用此釋出者和訂閱者模型迫使我們採用最終一致性範例。

  • 樂觀併發

當轉向微服務時,我們不應過多擔心併發性,因為很多東西都是非同步的,類似於兩階段提交情況。我們應該嘗試構建工作流,如果由於併發而出現問題,我們可以反向執行。

DDD是一個古老的主題。如果您還沒有閱讀“藍皮書”,我建議您這樣做,因為這是圍繞這一主題的最著名的書。

基本上,DDD和一些稱為“ 事件溯源 ”的技術在微服務方面有所幫助,它有助於我們瞭解一個服務的結束位置以及其他服務的啟動位置。它基本上有助於我們根據業務能力瞭解我們的服務,並幫助我們瞭解每項服務的界限。

DDD和Event Sourcing也將幫助我們構建一種名為“ Ubiquitous Language ”的語言,這種語言是開發人員和使用者之間的一種通用、嚴謹的語言。
  • NoSQL:讀模型

如前所述,事件溯源被大多數書籍推薦為在我們的系統中儲存事件的模式(由我們的CQRS模式的命令路徑處理)。現在這對於查詢目的而言將非常緩慢,因此建議使用“ 讀取模型 ”,它本質上是我們資料的模型。但出於效能原因,建議使用NoSQL或記憶體快取作為微服務中的“讀取模型”。
  • API閘道器:針對UI進行了最佳化

在單體架構中,UI部分很簡單。但在微服務中,UI需要對不同的服務進行多次呼叫,並收集資料並將其組合在一起呈現給使用者。這也是我們在身份驗證和授權方面的“可信子系統”邊界。
要解決此問題,可以使用不同的工具來處理多個呼叫並聚合資料(就像它是Monolth一樣)。
API Gateway和GraphQL是用於解決此問題的兩種技術。但對我而言,這個API閘道器或GraphQL將是瓶頸和故障的單點,它必須改變並與微服務中的所有變化同步。我還不清楚這實際上是不是一件好事!
另一方面,如果你沒有這個層,並讓你的UI直接與你的微服務交談,那麼無論何時你移動微服務,或者你拆分它,那麼你需要不斷更改UI,這也不行!
  • 版本控制:API,資料庫

在微服務中,我們需要提前考慮版本控制,因為事情會發生變化,我們需要能夠正確處理它們。

需要的軟體和庫
在語言和框架方面,使用C#和Asp.Net Web API,以下是我用於此實現的軟體和nuget包庫。

1. Redis
我將使用Redis作為我們的Read Model和NoSQL Storage。它非常易於安裝和配置。
就Nuget包而言,為了與C#中的Redis互動,我使用StackExchange.Redis nuget包作為Redis客戶端。

2. RabitMQ
對於我們的“訊息匯流排”元件,我將使用RabitMQ。它是一個非常著名,廣泛使用的開源訊息代理,它是配置方面最簡單的一個。
注意:Azure Service Bus是另一種選擇,如果您計劃將Microsoft Azure用作雲基礎架構,則可能是更好的選擇。
對於與RabbitMQ互動的Nuget庫,我使用EasyNetQ庫作為RabbitMQ客戶端,為我們的目的提供了非常好的API。


3. EventStore
EventStore作為一個工具,在為他們的工具拿起一個名字方面做得非常好......事實證明它是一個Event Store!
我將使用它作為我們的事件儲存,以保持我們的Domain Microservice使用的事件歷史記錄。

我們為什麼要使用Event Store?
我們使用的任何事件儲存都有自己的一套特性和功能,但是當涉及到將事件儲存用作微服務中的資料儲存時,有一個主要原因:

無需處理“兩階段提交”

想象一下,如果我們不使用Event Store,而使用常規SQL資料庫以及Service Bus來發布要訂閱的其他服務的事件。在這種情況下,我們需要正確處理“2階段提交”情況。

如果您在某些資料庫表中儲存了一些記錄,然後嘗試將相關事件釋出到Service Bus,那麼您怎麼辦?現在你必須使用“ 指數補償Exponential backoff ”演算法重試一段時間(這會導致系統延遲正確傳播事件),如果你最後沒有成功釋出事件,你必須回滾您對資料庫所做的更改。

我們可以使用常規資料庫嗎?
簡短回答是,如果資料庫具有Publisher Subscriber功能。
可以為我們提供Pub / Sub功能以及常規儲存的最著名的NoSQL資料庫是Redis

我們可以使用沒有Pub / Sub功能的資料庫嗎?
答案是肯定的,但是你需要有一種跨越資料庫事務的“ 補償事務交易 ”,並向服務匯流排釋出訊息,只有當兩個操作都成功時才完成交易。雖然它不會是一個原子操作,卻可以工作,但是你可以想象,如果我們能夠在沒有事務的情況下做到這一點,那麼效率會更高。
來自維基百科:
如果事務是長期存在的(通常稱為Saga事務),例如在需要使用者輸入的業務過程中,也使用補償事務。在這種情況下,資料將被提交到永久儲存,但隨後可能需要回滾,可能是由於使用者選擇取消操作。

使我們使用Redis,我們仍然需要一個Redis事務,我們只是將兩個操作(1.向Redis新增資料2.釋出一個事件)放在事務範圍內。

Azure Service Bus的原子事務
以下內容來自Azure Service Bus Github頁面,該頁面解釋了補償或Saga事務當前支援的內容:
Azure Service Bus當前不支援透過MS DTC或其他事務協調器登記到分散式兩階段提交事務,因此您無法在同一事務範圍內對SQL Server或Azure SQL DB和Service Bus執行操作。
Azure Service Bus支援.NET Framework事務,它將易失參與者置於事務範圍內。因此,一組服務匯流排操作是否會生效可以取決於獨立登記的並行本地工作的結果。

使用事件溯源是原子的Atomic
使用Event Store,我們不需要擔心補償或Saga事務,因為我們正在做的只有一件事,即將事件追加儲存到事件儲存中,因此無論如何它都是原子操作,因此根本不需要事務處理。

使用事件溯源的任何其他原因?
嗯,是的,Event Stores提供的一些功能非常有用:

  1. 準確的審計跟蹤

當我們在事件儲存中單獨記錄每個事件時,透過檢視流,很明顯每個流的歷史記錄是什麼,以及為什麼我們處於當前狀態。
我們不能用SQL做到這一點嗎?是的,我們可以使用自定義實現或SQL CDC(更改資料捕獲)來執行完全相同的操作,但Event Store可以更輕鬆地完成此操作。
嗯,是的,Event Stores提供的一些功能非常有用:
  1. 準確的審計跟蹤當我們在事件儲存中單獨記錄每個事件時,透過檢視流,很明顯每個流的歷史記錄是什麼,以及為什麼我們處於當前狀態。我們不能用SQL做到這一點嗎?是的,我們可以使用自定義實現或SQL CDC(更改資料捕獲)來執行完全相同的操作,但Event Store可以更輕鬆地完成此操作。
  2.  易於進行時間查詢因為我們為每個業務物件保留完整的事件跟蹤,所以很容易查詢過去並知道過去任何時候該物件的狀態。

我們不能用SQL做到這一點嗎?是的,我們可以,但使用Event Stores實現此類查詢要容易得多。

我們需要Event Store和Service Bus嗎?
簡短的回答是否定的。但是,如果您已經在您的微服務的常規資料庫和釋出訊息到服務匯流排之間實施了適當的Saga或補償事務處理,並且您沒有查詢時態資料或非常準確的審計日誌跟蹤的要求,並且您不關心您的歷史資料中的內容會發生變化,您只需使用服務匯流排,就不需要事件儲存。

為什麼要將事件釋出到Event Store和Service Bus?
你不必這樣做,如果您的服務有一些要求,使用事件儲存是一個很好的選擇,然後同時你的系統中有一些其他服務,不需要事件儲存,他們只是使用服務匯流排和他們有自己的本地SQL資料庫,所以在這種情況下,您將事件釋出到Event Store以使用Event Store的服務,然後將另一個事件釋出到Service Bus以獲取不使用Event Store,僅使用Service的服務匯流排和常規SQL或NoSQL資料庫。

我們可以一起跳過Event Sourcing嗎?
簡短的回答是肯定的,但只有當你考慮了它的所有好處和用例並且你沒有任何這些要求時,你也已經為跨服務交易處理實施了適當的“Saga”。
根據我的經驗,在大多數企業軟體中都有完全適合Event Sourcing提供的業務需求,所以我猜你最終可能會使用Event Store或者使用常規SQL或NoSQL資料庫自己實現類似的東西。

財務元件系統是第一個被認為適合使用Event Stores的元件。

使用Event Sourcing來解決所有微服務的問題?

使用事件源需要考慮的因素,如報告,較少的社群支援或與常規資料來源相比較不成熟的工具,與人們習慣的相比較尷尬的查詢等。
所以考慮到這一點,我不認為企業架構中的每一個微服務都需要使用事件源,就像軟體工程中的任何其他模式一樣,我們應該務實,只需使用最有意義的工具和模式。
回到微服務的定義,我們的想法是每個服務或團隊將決定以最有效的方式提供業務需求最有意義的東西,我相信在特定的微服務中使用或不使用事件源是類似的需要由團隊根據業務需求做出的決定。

點選標題檢視原文系列,可以在github上找到完整的程式碼

相關文章