需要注意的是標題中的CAP不是指的CAP理論,而是園區大神楊曉東實現的框架,CAP框架基於本地訊息表用最終一致性實現分散式事務。
本地訊息表
首先我們考慮一個場景,在將使用者資訊更改後,需要傳送一條訊息到訊息佇列、快取或是寫入到其他庫中。這個過程涉及到一個本地庫與MQ、本地庫與Cache或是本地庫與其他庫兩者之間的事務問題,不能用簡單的資料庫事務控制了。
這種分散式事務下,常用的解決方案有2PC、3PC等強一致性保證的,也有TCC、Sagas模型、本地訊息表、內嵌本地訊息表的MQ等最終一致性保證的。
而在很多非同步場景下,允許系統存在短暫的不一致,只需達到最終一致,比起強一致性那種剛性事務,採用柔性事務,在很多場景下更有利於我們去實現。
執行過程
在使用CAP框架前,先熟悉下作為分散式事務解決方案之一的本地訊息表工作過程。
- 訊息發起方(如圖左側部分)和訊息接收方(如圖右側部分),先額外建一套訊息表,用來記錄及跟蹤訊息內容及狀態。
- 當有請求到訊息發起方時,處理完業務邏輯釋出訊息將業務資料和訊息資料一同提交到本地表中,此時為本地事務。
- 本地事務沒有問題後,將訊息傳送到MQ傳遞給訊息消費方。如果訊息傳送失敗,會進行重試傳送。
- 訊息消費方,接收並處理訊息,完成自己的業務邏輯,此時為訊息消費方本地事務,如果本地事務完成,則更改接收訊息的狀態,更改本地,如果處理失敗,那麼可再次重試執行。
- 最終,左側事務與右側事務達到最終一致。
CAP框架
CAP是一個在分散式系統中(SOA,MicroService)實現事件匯流排及最終一致性(分散式事務)的一個開源的 C# 庫,具有輕量級,高效能,易使用等特點。
- 具有 Event Bus 的所有功能,提供了更加簡化的方式來處理EventBus中的釋出/訂閱。
- 具有訊息持久化的功能,當服務進行重啟或者當機時,可以保證訊息的可靠性。
- 基於本地訊息表實現了分散式事務中的最終一致性。
- 整合了視覺化頁面方便觀察訊息狀態。
- 提供了一系列Nuget包以選擇需要的工具接入。
- 第一個包DotNetCore.CAP為必須要安裝的。
- 可以依據訊息佇列的不同選擇用RabbitMQ、Kafka或是AzureServiceBus等。
- 根據服務使用的資料庫情況選擇需要將本地訊息表落庫,可以選擇SqlServer、MySql、PostgreSql、MongoDB等,或是直接使用記憶體儲存,方便快速實踐。
場景案例
依照EShopOnContainers中的一張圖來實現一個例子,使用者更新使用者資訊,將更新的部分通過事件傳送到訊息佇列中,下游的購物車和訂單服務偵聽到訊息,更改買家資訊。
在此基礎上行,設計三個上下文,並分別整合CAP,藉助RabbitMQ作為訊息佇列,對於UserService、BasketService和OrderService,都直接使用了資料庫(當然可以不僅限於資料庫)。
服務建立
專案建立
開始建立幾個服務,新建空白解決方案,依次建立三個WebApi專案,並移除預設的控制器。
簡單設計下,在三個服務中建立三個DbContext,對應三個獨立的資料庫。
- UserService中建立UserInfo實體及UserDbContext
- BasketService中建立Basket實體及BasketDbContext
- OrderService中建立Order實體及OrderDbContext
安裝Nuget包
在三個服務中均安裝完如下選中的包,此次Demo中為方便快速實踐,選擇RabbitMQ作為訊息佇列,MySql作為資料庫儲存。
對於EFCore及MySql包,安裝瞭如下幾個包
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.Design
Microsoft.EntityFrameworkCore.Tools
Pomelo.EntityFrameworkCore.MySql
注意此處EFCore中MySql版本和CAP中MySql版本兩者間依賴的MySqlConnector不一致會有點問題
配置服務
需要對CAP進行設定,比如使用的是什麼資料庫、什麼訊息佇列及配置下訊息佇列引數,這一系列初始化設定在Startup.cs中配置好。
- ConfigureService中配置DbContext和CAP服務
- Configure中CAP的引入中介軟體
- 利用EFCore的遷移命令生成下資料庫遷移指令碼,將DbContext內實體生成到資料庫中
- 單個服務啟動後,CAP元件會將內建表建立到資料庫中。
- 服務全部啟動後,RabbitMQ Client會自動註冊到RabbitMQ Server中同時建立好給定的Exchange(不給定則使用預設值),存在訂閱的服務則註冊佇列繫結到給定的Exchange下。
釋出事件
在 UserService中UserController 中注入ICapPublisher,使用Patch介面更新一個Address,然後使用ICapPublisher釋出一條訊息。
- 更新本地User表內資訊。
- 藉助_capPublisher釋出事件,先將事件資訊記錄到本地MqPublish表。
- 前兩步都是針對本地表操作,一個事務保證,寫入MqPublish成功後再由CAP將記錄傳送到RabbitMQ中。
訂閱事件
在BasketService和OrderService中完成事件的訂閱。各自新建了一個Handler來處理訊息。在Handler中對處理的方法加上CapSubscribe特性,其中監聽的是釋出事件時傳送的事件名或訊息名。
- BasketService收到RabbitMQ中的訊息,CAP將訊息寫入到MqReceive中。
- 呼叫相應的Handler處理事件。
- 更新Basket本地表,本地事務完成被提交。
- CAP元件將本地的MqReceive相關記錄更改狀態到完成,如本地事務提交失敗,則再次重試。
總結
拋棄強一致性想法藉助最終一致性完成,將分散式事務拆分成多個本地事務進行處理。採用最終一致性來使得所有本地事務完成,即使部分出現失敗,也可重試,如重試機制無效最終藉助人力完成。
在非同步場景下,CAP及其方便了我們去處理分散式事務的過程。
當前RabbitMQ場景下,當某個服務做多個部署時,同一個佇列仍能保證一個消費者消費。這也避免了有些場景下,需要對資源加鎖來防止同時消費場景。
參考
2021-04-28,望技術有成後能回來看見自己的腳步