https://github.com/OpenSagas-csharp/servicecomb-pack-csharp
Saga基本使用指南
使用前置條件說明
如果還有同學對Saga還不甚了解的同學,可以參考Saga官方中文地址地址,同時可以參考此項目貢獻者之一的WithLin的一篇中文說明文章,該地址如下:地址,文章由淺入深的講述了分布式事務在微服務場景下的重要性,以及Saga對分布式事務的大致實現方式和后續的思考
- 必須 你需要可用的一個本地或者遠程的數據庫(mysql或者postpresql)作為Saga持久化分布式事務事件的持久化存儲,當然只要官方支持的Database Provider即可,具體idea數據庫配置如下圖,注意數據庫的名字與您真實數據庫名一致
- 必須 成功啟動alpha-server,導致環境搭建以及部署頗為麻煩,后期官方將會提供image上傳docker hub提供給大家使用,啟動成功參考下圖
-
可選 同時saga提供了UI可視化界面,直接idea中啟動saga-web即可
-
支持docker-compose:根目錄提供了一個dockercompose文件,只需要在工程的根目錄下執行docker-compose up -d 即可,上面的操作可以給感興趣的調試環境的搭建。
開始玩轉分布式事務Saga
克隆當前項目,然后請使用VS2017打開解決方案定位到sample目錄,你會看到如下所示的三個實例應用程序,這里app都是基於TargetFramework=netcoreapp2.0的,所以需要相應的啟動環境
下面對上圖做一個基本介紹,假定現在我們有三個微服務,分別是 Booking-預定服務,Car-訂車服務,Hotel-酒店服務,相信大家一看便知,三者的從屬關系以及在現實社會中的關聯關系,下面我們的分布式事務一致性測試將在這幾個app中完成,現在我們分別對項目做一定的初始化工作
services.AddOmegaCore(option => { // your alpha-server address option.GrpcServerAddress = "localhost:8080"; // your app identification option.InstanceId = "Booking123"; // your app name option.ServiceName = "Booking"; });
這里需要對三個項目都做如上所示的基本配置即可,現在一直都配置就緒了,下面開始我們的分布式事務的測試吧...
分布式事務場景測試
下面將會針對正常以及異常情況分別測試
正常情況測試
[HttpGet, SagaStart] // SagaStart 顧名思義標記為分布式事務開始的地方 [Route("book1")] public ActionResult Book() { // init basic httpclient var httpClient = new HttpClient(); // mark a reservation of car httpClient.GetAsync("http://localhost:5002/api/values"); // book a hotel httpClient.GetAsync("http://localhost:5003/api/values"); // your busniess code // for example save the order to your database return Ok("ok"); }
請求結果返回"ok",結果我們看看數據記錄的是什么東西,相信也是大家比較關心的,看懂了數據庫也就了解了分布式事務的階段性事件存儲,下面直接上圖:
我們從上圖很明顯的就能看出來服務的先后關系以及服務之間的依賴關系,同時LocalTxId標記了每個一個過程的唯一ID,其中[type]需要注重說明一下,標記了每個動作了的狀態同時也是判斷每個微服務是否成功是否需要補償的重要標准( 特別說明: sample項目中的預定車輛以及預定酒店是模擬操作,具體可以參見各自項目代碼 )
節點服務異常情況
這里我為什么要說節點服務異常呢?相信經歷過微服務的同學就知道,錯綜復雜的服務之間的調用,就會增加耦合以及某個節點服務出現異常導致整個調用連失敗的情況,所以基於如此我們下面測試每個階段所帶來的情況分析
[HttpGet, SagaStart] [Route("book1")] public ActionResult Book1() { // throw new a exception for test throw new DbUpdateException("I'm a dbUpdateException", new Exception()); // init basic httpclient var httpClient = new HttpClient(); // mark a reservation of car httpClient.GetAsync("http://localhost:5002/api/values").Wait(); // book a hotel httpClient.GetAsync("http://localhost:5003/api/values").Wait(); return Ok("ok"); }
這里只是記錄[Book1]本身服務的生命周期,因為還沒有請求car和hotel,下面截圖也驗證我的預期結果:
測試Car-Service調用異常
這里需要特別說明的是,需要 httpClient 等待微服務調用結果,這樣car-service出現調用異常,我們的框架才會感知到,才會上報通知事務管理,最后終止或者回滾事務鏈條
Omega.Sample.Booking -> ValuesController:
[HttpGet, SagaStart] [Route("book2")] public ActionResult Book2() { // init basic httpclient var httpClient = new HttpClient(); // mark a reservation of car , this will be throw a exception from car-service httpClient.GetAsync("http://localhost:5002/api/values").Wait(); // book a hotel httpClient.GetAsync("http://localhost:5003/api/values").Wait(); return Ok("ok"); }
Omega.Sample.Car -> ValuesController :
[HttpGet]
public IEnumerable<string> Get() { CarBookingService carBookingService = new CarBookingService(); var carbook = new CarBooking() { Id = 1, Amount = 1, Name = "WithLin" }; carBookingService.Order(carbook); return new string[] { "value1", "value2" }; }
Omega.Sample.Car -> CarBookingService:
public class CarBookingService { private readonly ConcurrentDictionary<int, CarBooking> _bookings = new ConcurrentDictionary<int, CarBooking>(); [Compensable(nameof(CancelCar))] public void Order(CarBooking carBooking) { carBooking.Confirm(); _bookings.TryAdd(carBooking.Id, carBooking); // throw new Exception throw new Exception("test car serivice error"); } void CancelCar(CarBooking booking) { _bookings.TryGetValue(booking.Id, out var carBooking); carBooking?.Cancel(); } }
果不其然我們的[Book2]方式直接向我們拋出了異常,上圖說明:
那么再來看看數據是否和我們預期感覺一樣訥,相信聰明的小伙伴應該知道套路是什么了:
關於 car-service 的紅框狀態描述相信大家就很清楚了,歷經了開始->中止->結束,最后整個 Booking 方式完成
測試Hotel-Service調用異常
預期調用 Hotel-Service 異常,但是我們的 car-service 調用成功,這個時候我們需要 car-service 通過補償的方式撤銷調用 car-service 帶來的數據或者狀態的變化,達到要么全部成功,要么全部失敗的結果,實現最終一致性
Omega.Sample.Booking -> ValuesController:
[HttpGet, SagaStart] [Route("book")] public async Task<ActionResult> Book() { // init basic httpclient var httpClient = new HttpClient(); // mark a reservation of car await httpClient.GetAsync("http://localhost:5002/api/values"); // book a hotel await httpClient.GetAsync("http://localhost:5003/api/values"); return Ok("ok"); }
Omega.Sample.Car -> ValuesController :
[HttpGet]
public IEnumerable<string> Get() { CarBookingService carBookingService = new CarBookingService(); var carbook = new CarBooking() { Id = 1, Amount = 1, Name = "WithLin" }; carBookingService.Order(carbook); return new string[] { "value1", "value2" }; }
Omega.Sample.Car -> CarBookingService:
這里需要特別說明 [Compensable(nameof(CancelCar))] 此標記是指示補償或者回滾時的方法,當然它的執行是在事務官發起通知的時候執行,具體參考下圖斷點命中
public class CarBookingService { private readonly ConcurrentDictionary<int, CarBooking> _bookings = new ConcurrentDictionary<int, CarBooking>(); [Compensable(nameof(CancelCar))] public void Order(CarBooking carBooking) { carBooking.Confirm(); _bookings.TryAdd(carBooking.Id, carBooking); //throw new Exception("test car serivice error"); } void CancelCar(CarBooking booking) { _bookings.TryGetValue(booking.Id, out var carBooking); carBooking?.Cancel(); } }
Omega.Sample.Hotel -> ValuesController :
[HttpGet]
public IEnumerable<string> Get() { HotelBookingService bookingService = new HotelBookingService(); HotelBooking hotelBooking = new HotelBooking() { Id = 1, Amount = 10, Name = "test" }; bookingService.Order(hotelBooking); return new string[] { "value1", "value2" }; }
Omega.Sample.Hotel -> HotelBookingService:
注意這里因為 booking.Amount > 2 將會觸發異常導致服務調用錯誤
[Compensable(nameof(CancelHotel))] public void Order(HotelBooking booking) { if (booking.Amount > 2) { throw new ArgumentException("can not order the rooms large than two"); } booking.Confirm(); _bookings.TryAdd(booking.Id, booking); }
Car-Service 補償方法觸發執行命中斷點如下圖:
數據庫事務鏈條狀態圖(聰明的你觀察到了那個補償事件的記錄了訥):
測試Booking-Service 最后調用異常
這里我們最后再來測試一下,如下情況,也是前面的 car-service hotel-service 調用都沒有問題,但是最后 booking-service 提交的出現了未知異常(例如網絡抖動.數據庫閃斷之類),代碼我就不貼太多了,展示 booking-service 即可
[HttpGet, SagaStart] [Route("book")] public async Task<ActionResult> Book() { // init basic httpclient var httpClient = new HttpClient(); // mark a reservation of car (no exception) await httpClient.GetAsync("http://localhost:5002/api/values"); // book a hotel (no exception) await httpClient.GetAsync("http://localhost:5003/api/values"); throw new Exception("just test unknown exception"); return Ok("ok"); }
理所當然的如下圖所示:
關於 Command 表大致用來存儲需要補償的命令的,相應的alpha-server有定時服務在刷這個表,一直補償成功為止,來保證最終的分布式事務的一致性
相信你已經注意到了我們的兩個預定服務都觸發了補償邏輯(紅框所示)
如何在服務超時情況維持分布式事務測試
[HttpGet, SagaStart(TimeOut = 3)] [Route("book3")] public ActionResult Book3() { // init basic httpclient var httpClient = new HttpClient(); // mark a reservation of car , this will be throw a exception from car-service httpClient.GetAsync("http://localhost:5002/api/values").Wait(); // book a hotel httpClient.GetAsync("http://localhost:5003/api/values").Wait(); Thread.Sleep(5000); return Ok("ok"); }