概念
什麼是事務
事務是由一組操作組成的一個工作單元。
事務特性
原子性:事務內部的一組操作要麼同時成功,要麼同時失敗
隔離性:不同事務之間是互相不影響的
一致性:事務內部一組操作,各自操作產生的結果數據,要能夠保證都是預期的狀態
持久性:事務內部一組操作,各個操作產生的數據要能夠持久的效應
什麼是分佈式事務
分佈式事務就是一組服務操作的集合
例如:在分佈式系統或者微服務系統內,完成一個任何,需要涉及到多個服務來共同完成,這一組服務操作組成的集合,就是分佈式事務
分佈式事務類型
1. 不同服務不同數據庫
2. 不同服務相同數據庫
3. 相同服務不同數據庫
事務分類和分佈式事務演化
剛性事務
分爲兩階段提交和三階段提交
剛性事務適用於-----相同服務不同數據庫-----的場景
剛性事務---兩階段
事務參與者: 所有需要操作到的服務都是事務參與者
事務協調者: 統一協調參與者的事務
第一階段: 準備階段 prepare : 事務協調者向所有事務參與者詢問是否準備好,並且參與者準備好之後返回yes
第二階段: 提交階段 commit :事務協調者向所有事務參與者提交,並且參與者提交成功之後返回ack
剛性事務---三階段
在兩階段的基礎上在最前面新增了一個確認階段
第一階段:確認階段 canCommit :事務協調者向所有事務參與者確認服務是否正常,並且參與者確認好之後返回yes
第二階段: 準備階段 preCommit : 事務協調者向所有事務參與者詢問是否準備好,事務參與者寫提交日誌,並且參與者準備好之後返回ack
第三階段: 提交階段 doCommit :事務協調者向所有事務參與者提交,並且參與者提交成功之後返回havaCommit
缺點:
1. 同步阻塞: 如果其中一個階段其中一個服務出現問題,會導致其他服務阻塞,所以性能低
2. 數據不一致: 如果提交階段其中一個服務提交失敗,未能返回ack ,那麼就會造成數據的不一致
3. 單點故障: 如果事務協調者出現異常,會造成所有的服務阻塞
雖然會有這麼多缺點,但是都是在微服務之間異常或者通信異常導致的,同一個服務就不會存在這個問題,所以
剛性事務適用於-----相同服務不同數據庫-----的場景
柔性事務
就是不完全遵守事務4特性的分佈式事務-----主要體現在一致性(不完全一直,最終一致性)
基於CAP理論以及BASE理論
Base理論核心思想 :理論的核心思想就是:我們無法做到強一致,但每個應用都可以根據自身的業務特點,採用適當的方式來使系統達到最終一致性
可查詢操作:服務操作具有全局唯一標識,操作唯一確定的時間
冪等操作:重複調用多次產生的業務結果與調用一次產生的結果相同,一是通過業務操作實現冪等性,二是系統緩存所有請求與處理結果,最後是檢測到重複請求之後,自動返回之前的處理結果。
柔性事務可以分爲
1. 同步事務(http,rpc) :Tcc分佈式事務 和 Saga分佈式事務
2. 異步事務(消息隊列MQ)
柔性事務之TCC
操作數據庫會產生三個數據狀態:1、未確認狀態,2、確認狀態,3、取消未確認狀態
Tcc分爲子事務和全局事務
子事務:每個要操作的服務都是一個子服務, 分爲三個階段:
1、Try階段 : 所有要操作的服務中都要先執行一個try階段,把需要添加的數據進行預添加,此時會將改數據標記爲未確認的狀態
2、Confirm階段 :所有子服務的try階段成功之後,執行Confirm階段,把需要添加的數據進行添加,此時會將改數據標記爲確認狀態
3、Cancel階段:如果在第二階段出現某個子服務異常,會通知其他服務進行回滾,將第一步Try階段添加的預添加數據進行刪除
全局事務:多個子事務的集合, 操作到的第一個服務就會產生一個全局事務,每次同子服務交互都會帶上全局服務的ID進行關聯
優點
1.解決了跨服務的業務操作原子性問題,例如組合支付,訂單減庫存等場景非常實用
2.TCC的本質原理是把數據庫的二階段提交上升到微服務來實現,從而避免了數據庫2階段中鎖衝突的長事務低性能風險。
3.TCC異步高性能,它採用了try先檢查,然後異步實現confirm,真正提交的是在confirm方法中。
缺點
1.對微服務的侵入性強,微服務的每個事務都必須實現try,confirm,cancel等3個方法,開發成本高,今後維護改造的成本也高。
2.爲了達到事務的一致性要求,try,confirm、cancel接口必須實現等冪性操作。
(定時器+重試)
3.由於事務管理器要記錄事務日誌,必定會損耗一定的性能,並使得整個TCC事務時間拉長,建議採用redis的方式來記錄事務日誌。
TCC每個子事務會自己執行,不會造成阻塞,不會造成性能消耗過大,就算其中一個子事務造成了異常,會產生一個重試機制不停的重試,從而達到最終一致性
所以TCC適用於微服務-----( 不同服務不同數據庫、不同服務相同數據庫)-----的場景
柔性事務之Saga
子事務:每個要操作的服務都是一個子服務, 分爲兩個階段:
第一個階段(Ti): 直接執行業務階段, 直接像數據庫中添加數據,添加成功後向協調器返回 成功
第二個階段(Ci): 直接取消階段,第一個執行階段所有服務都執行成功了就不會執行第二個取消階段,當第一個階段其中某個服務失敗了就會執行子事務中的Ci取消階段,然後向協調器發送命令,協調器向其他執行成功的子服務也發送命令
優點
1、避免服務之間的循環依賴,因爲saga協調器會調用saga參與者,但參與者不會調用協調器
2、集中分佈式事務編排
3、降低參與者的複雜性
4、回滾更容易管理
Saga模式的一大優勢是它支持長事務。因爲每個微服務僅關注其自己的本地原子事務,所以如果微服務運行很長時間,則不會阻止其他微服務。這也允許事務繼續等待用戶輸入。此外,由於所有本地事務都是並行發生的,因此任何對象都沒有鎖定。
缺點
協調器集中太多邏輯的風險
Saga模式很難調試,特別是涉及許多微服務時。此外,如果系統變得複雜,事件消息可能變得難以維護。Saga模式的另一個缺點是它沒有讀取隔離。例如,客戶可以看到正在創建的訂單,但在下一秒,訂單將因補償交易而被刪除
Saga結合了剛性事務和TCC一些優勢,但是相對於TCC沒有那麼複雜,相對於剛性事務中事務協調器做了集羣, 每個子事務會自己執行,不會造成阻塞,不會造成性能消耗過大,就算其中一個子事務造成了異常,會產生一個重試機制不停的重試,從而達到最終一致性
所以TCC適用於微服務-----( 不同服務不同數據庫、不同服務相同數據庫)-----的場景
使用saga的 ServiceComb Pack 框架構建微服務
Saga Pack 架構是由alpha和omega組成,其中:alpha充當協調者的角色,主要負責對事務進行管理和協調。
服務端omega是微服務中內嵌的一個agent,負責對網絡請求進行攔截並。客戶端向alpha上報事務事件。saga數據庫,存儲事務參與者的事務數據(mysql,postsql)
環境搭建
搭建 Alpha 服務端環境
1. 安裝 Java Jdk
2. 下載 ServiceComb Pack , 下載後解壓
3. 安裝 Mysql 或者 PostgreSQL 數據庫。 由於ServiceComb Pack 只支持兩種數據庫,我這裏就用的mysql
4. 官網下載 mysql 的數據庫Java連接驅動 ,我這裏用的是 mysql-connector-java-8.0.15.jar
5. 在解壓好的 ServiceComb Pack 根目錄下面創建一個 plugins 的文件夾,文件夾中將第四步的驅動放在這個文件夾中
6. 在mysql 中創建一個名爲 saga 的數據庫, 在當前目錄使用CMD ,然後運行命令,注意修改數據庫地址信息和賬號密碼
java -D"spring.profiles.active=mysql" -D"loader.path=./plugins" -D"spring.datasource.url=jdbc:mysql://localhost:3306/saga?useSSL=false&serverTimezone=Asia/Shanghai" -D"spring.datasource.username={賬號}" -D"spring.datasource.password={密碼}" -jar alpha-server-0.5.0-exec.jar
如圖,運行成功,表示 alpha 服務端搭建成功了,數據庫也會生成相關表結構
可以看到生成的表不僅有Saga ,還有 Tcc, 說明ServiceComb Pack 同時還兼容Tcc協議,這裏我們用不到Tcc,直接忽略Tcc的表
搭建 Omega 環境
由於Nuget源中沒有引入Omega,所以我們只有下載源碼之後,把源碼引入我們的文件當中。源碼中還是測試項目示例,可以看看使用
Github 上下載 Omega c# 源碼
分佈式事務示例
簡單的使用分佈式事務
1. 在我們 項目中引入源碼中Src中的項目
2. 在我們需要用到分佈式事務項目中引入兩個 Servicecomb.Saga.Omega.Core 和 Servicecomb.Saga.Omega.AspNetCore
3. 注入服務
services.AddOmegaCore(option => { option.GrpcServerAddress = "localhost:8080"; // 1、協調中心地址 option.InstanceId = $"ConsulApi-1";// 2、服務實例Id option.ServiceName = $"ConsulApi";// 3、服務名稱 });
4. 分佈式事務開始的接口方法上面打上特性 [SagaStart] 。 我這裏懶沒有用多個服務,就用的本身這個consulapi運行了多個不同端口的實例模擬多個服務
[SagaStart] [HttpGet("AddUser")] public async Task<IActionResult> AddUser() { //獲取當前端口,根據端口號的不同進行不同的業務 var localPort = Request.HttpContext.Connection.LocalPort; if (localPort == 5001) { HttpClient client = new HttpClient(); client.BaseAddress = new Uri("http://localhost:5002"); await client.GetAsync("/api/User/AddUser"); HttpClient client2 = new HttpClient(); client2.BaseAddress = new Uri("http://localhost:5003"); await client2.GetAsync("/api/User/AddUser"); return Ok("添加成功"); } else if (localPort == 5002) { _userService.Create(new UserInfo() { Name = "Darcy", Age = 18 }); } else if (localPort == 5003) { _cityService.Create(new City() { CityName = "成都市" }); } return Ok("添加失敗"); }
5. 在每個操作數據庫的方法上面打上 特性 [Compensable(nameof(補償方法名稱))] 。 注意這裏有個坑,補償方法一定不要用public ,否則會報未將對象引用到實例
private string connStr = "server=localhost;port=3306;user=root;password=hua3182486;database=fcbsaga;SslMode=none;"; [Compensable(nameof(Delete))] public void Create(UserInfo model) { using (var conn=new MySqlConnection(connStr)) { conn.Execute($"insert into UserInfo(name, sex) values(@name,@age)",model); } } void Delete(UserInfo model) { using (var conn = new MySqlConnection(connStr)) { conn.Execute($"delete from UserInfo where id ='{model.Id}'"); } }
6. 添加程序集信息文件 AssemblyInfo.cs
using Servicecomb.Saga.Omega.Abstractions.Transaction; [module: SagaStart] [module: Compensable]
7. 重點: Nuget 引入 MethodDecorator.Fody ,然後重新生成項目,會生成 FodyWeavers.xml 文件 。 fody會在生成IL時將一些代碼自動生成進去,這裏目的是爲了在執行方法前後去執行 SagaStart 和 Compensable ,方便協調器監控各個子事務,去判斷是否執行補償機制。(官方也沒有看到這個操作,花了兩天時間踩坑,羊了個羊)
Nuget: MethodDecorator.Fody
運行之後,可以看到數據庫裏面添加了幾條記錄,從數據中可以看出,在SagaStart開始時會生成一個全局事務ID,然後會把全局事務ID傳給 Compensable 中,Compensable也會生成一個子事務ID,和全局事務ID關聯起來,這裏也就是執行了saga第一個階段TI ,如果子事務中出現了異常,就會通知全局事務ID,從而去觸發第二個補償階段。