文章目錄
- 一.為什么引入分布式事務?
- 二.理論基礎
- 1.CAP定理
- 2.BASE理論
- 三.Seata
- 1.微服務集成Seata
- 2.XA模式(掌握)
- 3.AT模式(重點)
- 4.TCC模式(重點)
- 5.Saga模式(了解)
- 四.四種模式對比
- 五.Seata高可用
一.為什么引入分布式事務?
事務的ACID原則
在大型的微服務項目中,每一個微服務都可能包含一個獨立的數據庫,當單個數據庫的操作由于某種原因進行回滾操作了,其他數據庫也會進行回滾操作嗎?
設想以下案例:
微服務下單業務,在下單時會調用訂單服務,創建訂單并寫入數據庫。然后訂單服務調用賬戶服務和庫存服務:
- 賬戶服務負責扣減用戶余額
- 庫存服務負責扣減商品庫存
但是當庫存數據庫中的庫存數量小于扣減數呢?庫存數據庫會回滾,但是賬戶服務的數據庫和訂單服務的數據庫依舊執行成功,這并不是我們希望看到的效果
所以說在分布式系統下,一個業務跨越多個服務或數據源,每個服務都是一個分支事務,要保證所有分支事務最終狀態一致,這樣的事務就是分布式事務。
二.理論基礎
1.CAP定理
1998年,加州大學的計算機科學家 Eric Brewer 提出,分布式系統有三個指標:
- Consistency(一致性)
- Availability(可用性)
- Partition tolerance (分區容錯性)
Eric Brewer 說,分布式系統無法同時滿足這三個指標。
這個結論就叫做 CAP 定理。
CAP定理- Consistency
Consistency(一致性):用戶訪問分布式系統中的任意節點,得到的數據必須一致
當進行數據更改的時候,node01的數據必須同步更新給node02,不然會導致查詢出的數據不一致
CAP定理- Availability
Availability (可用性):用戶訪問集群中的任意健康節點,必須能得到響應,而不是超時或拒絕
CAP定理-Partition tolerance
Partition(分區):因為網絡故障或其它原因導致分布式系統中的部分節點與其它節點失去連接,形成獨立分區。
Tolerance(容錯):在集群出現分區時,整個系統也要持續對外提供服務
所以說,Partition tolerance(分區容錯):即系統在網絡發生故障或分區時,仍然能夠保持正常的運行
假設ode03和node02之間出現了網絡故障,node01和node02是一個分區,而node03是一個分區
當出現分區時,如果想保證數據的一致性即Consistency,那么就必須等待node02和node03之間的網絡恢復,從而node02能將數據同步給node03,但此時不能滿足Availability (可用性),因為在等待網絡恢復過程中,nbode03不能夠進行訪問
如果想保證Availability (可用性),那么node02分區和node03分區的數據就會不一致,從而出現不一致性,即沒有滿足Consistency(一致性)
可以發現,由于兩兩互斥,所以三種特性都不能同時滿足
思考:elasticsearch集群是CP還是AP?
ES集群出現分區時,故障節點會被剔除集群,數據分片會重新分配到其它節點,保證數據一致。因此是低可用性,高一致性,屬于CP
2.BASE理論
BASE理論是對CAP的一種解決思路,包含三個思想:
-
Basically Available (基本可用):分布式系統在出現故障時,允許損失部分可用性,即保證核心可用。
-
Soft State(軟狀態):在一定時間內,允許出現中間狀態,比如臨時的不一致狀態。
-
Eventually Consistent(最終一致性):雖然無法保證強一致性,但是在軟狀態結束后,最終達到數據一致。
而分布式事務最大的問題是各個子事務的一致性問題,因此可以借鑒CAP定理和BASE理論:
- AP模式:各子事務分別執行和提交,允許出現結果不一致,然后采用彌補措施恢復數據即可,實現最終一致。
- CP模式:各個子事務執行后互相等待,同時提交,同時回滾,達成強一致。但事務等待過程中,處于弱可用狀態。
分布式事務模型
解決分布式事務,各個子系統之間必須能感知到彼此的事務狀態,才能保證狀態一致,因此需要一個事務協調者來協調每一個事務的參與者(子系統事務)。
這里的子系統事務,稱為分支事務;有關聯的各個分支事務在一起稱為全局事務
三.Seata
Seata是 2019 年 1 月份螞蟻金服和阿里巴巴共同開源的分布式事務解決方案。致力于提供高性能和簡單易用的分布式事務服務,為用戶打造一站式的分布式解決方案。
官網地址: http://seata.io/,其中的文檔、播客中提供了大量的使用說明、源碼分析。
Seata架構
Seata事務管理中有三個重要的角色:
-
TC (Transaction Coordinator) - 事務協調者:維護全局和分支事務的狀態,協調全局事務提交或回滾。
-
TM (Transaction Manager) - 事務管理器:定義全局事務的范圍、開始全局事務、提交或回滾全局事務。
-
RM (Resource Manager) - 資源管理器:管理分支事務處理的資源,與TC交談以注冊分支事務和報告分支事務的狀態,并驅動分支事務提交或回滾。
Seata提供了四種不同的分布式事務解決方案:
-
XA模式:強一致性分階段事務模式,犧牲了一定的可用性,無業務侵入
-
TCC模式:最終一致的分階段事務模式,有業務侵入
-
AT模式:最終一致的分階段事務模式,無業務侵入,也是Seata的默認模式
-
SAGA模式:長事務模式,有業務侵入
部署TC服務
需要注意:在配置Seata配置文件的時候,group和dataId必須和nacos配置管理的Data Id和Group一致
1.微服務集成Seata
1.引入seata相關依賴:
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId><exclusions><!--版本較低,1.3.0,因此排除--><exclusion><artifactId>seata-spring-boot-starter</artifactId><groupId>io.seata</groupId></exclusion></exclusions></dependency><!--seata starter 采用1.4.2版本--><dependency><groupId>io.seata</groupId><artifactId>seata-spring-boot-starter</artifactId><version>${seata.version}</version></dependency>
2.配置application.yml,讓微服務通過注冊中心找到seata-tc-server:
seata:registry:# TC服務注冊中心的配置,微服務根據這些信息去注冊中心獲取tc服務地址# 參考tc服務自己的registry.conf中的配置,# 包括:地址、namespace、group、application-name 、clustertype: nacosnacos: # tcserver-addr: 127.0.0.1:8848namespace: ""group: DEFAULT_GROUPapplication: seata-tc-server # tc服務在nacos中的服務名稱username: nacospassword: nacostx-service-group: seata-demo # 事務組,根據這個獲取tc服務的cluster名稱service:vgroup-mapping: # 事務組與TC服務cluster的映射關系seata-demo: SH
2.XA模式(掌握)
XA模式原理
XA 規范 是 X/Open 組織定義的分布式事務處理(DTP,Distributed Transaction Processing)標準,XA 規范 描述了全局的TM與局部的RM之間的接口,幾乎所有主流的數據庫都對 XA 規范 提供了支持。
seata的XA模式做了一些調整,但大體相似:
- RM一階段的工作:
- 注冊分支事務到TC
- 執行分支業務sql但不提交
- 報告執行狀態到TC
TC二階段的工作:
- TC檢測各分支事務執行狀態
- 如果都成功,通知所有RM提交事務
- 如果有失敗,通知所有RM回滾事務
RM二階段的工作:
- 接收TC指令,提交或回滾事務
XA模式的優點是什么?
- 事務的強一致性,滿足ACID原則。
- 常用數據庫都支持,實現簡單,并且沒有代碼侵入
XA模式的缺點是什么?
- 因為一階段需要鎖定數據庫資源,等待二階段結束才釋放,性能較差,低可用
- 數據操作導致的不一致需要進行回滾操作,故需要依賴關系型數據庫實現事務
實現XA模式
Seata的starter已經完成了XA模式的自動裝配,實現非常簡單,步驟如下:
1.修改application.yml文件(每個參與事務的微服務),開啟XA模式:
2.給發起全局事務的入口方法添加 @GlobalTransactional注解,本例中是OrderServiceImpl中的create方法:
3.AT模式(重點)
AT模式原理
AT模式同樣是分階段提交的事務模型,不過缺彌補了XA模型中資源鎖定周期過長的缺陷。
階段一RM的工作:
-
注冊分支事務
-
記錄undo-log(數據快照)
-
執行業務sql并提交
-
報告事務狀態
階段二提交時RM的工作:
- 刪除undo-log即可
階段二回滾時RM的工作:
- 根據undo-log恢復數據到更新前
例如,
一個分支業務的SQL是這樣的:update tb_account set money = money - 10 where id = 1
AT模式與XA模式最大的區別是什么?
XA模式一階段不提交事務,鎖定資源;AT模式一階段直接提交,不鎖定資源。
XA模式依賴數據庫機制實現回滾;AT模式利用數據快照實現數據回滾。
XA模式強一致;AT模式最終一致
AT模式的臟寫問題
假設有以下場景:
事務1和事務2都受AT模式的Seata管理,當事務1更改數據庫中的字段時(例如money字段),首先是獲取了DB鎖,保存了此刻的字段值快照,當執行完業務后歸還了DB鎖并提交了事務,但事務2緊接著獲得了DB鎖操作了相同的字段,也提交了事務,此時字段(money)就進行了兩次更改,但是事務1并不知道發生了除它以外的操作,更糟糕的是TC此時發現數據庫操作失敗需要進行回滾,事務1就立馬按照數據快照的數據進行更新數據庫,從而導致產生了臟寫問題
那么該如何避免臟寫問題呢?
AT模式的寫隔離
所謂的寫隔離其實就是對全局事務增加了全局鎖,全局鎖由TC記錄,當前正在操作某行數據的事務,該事務持有全局鎖,具備執行權。
還是剛才的例子,當業務1要提交事務之前,事務1拿到了全局鎖,擁有這個字段的執行權(即受AT模式管理的Seata中的所有事務,對這個字段的操作權有且僅有事務1),TC會將事務,表,字段等信息存入數據庫表中,當事務2想要操作相同字段并且提交的時候,它也需要獲取這個字段的全局鎖,但此時發現操作這個字段的全局鎖已經有事務拿到了,事務2就只能進行等待狀態(等待時間默認為30次,間隔為10ms,這樣做是為了避免事務1進行回滾而事務2占用DB鎖,但是事務1又占用了全局鎖所造成的死鎖狀態),當等待時間過去后,事務1拿到了DB鎖操作數據庫回滾完成,釋放全局鎖,這個中間字段只有事務1在進行操作,成功避免了臟寫問題
但是如果不是受Seata管理的事務2呢?因為不受Seata管理就沒有全局鎖,事務2就可以自由操作和事務1相同的字段了(前提是拿到DB鎖)
AT模式中引入了兩個快照機制,一個是before-image(操作前的數據快照)和after-image(操作后的數據快照),當事務1進行回滾的時候會判斷after-image是否和現在的字段數據一致,一致則回滾,不一致則記錄異常,發送警告,人工介入
AT模式的優點:
-
一階段完成直接提交事務,釋放數據庫資源,性能比較好
-
利用全局鎖實現讀寫隔離
-
沒有代碼侵入,框架自動完成回滾和提交
AT模式的缺點:
-
兩階段之間屬于軟狀態,屬于最終一致
-
框架的快照功能會影響性能,但比XA模式要好很多
實現AT模式
AT模式中的快照生成、回滾等動作都是由框架自動完成,沒有任何代碼侵入,因此實現非常簡單。
1.導入Sql文件:seata-at.sql,其中lock_table導入到TC服務關聯的數據庫,undo_log表導入到微服務關聯的數據庫
2.修改application.yml文件,將事務模式修改為AT模式即可
seata:data-source-proxy-mode: AT # 開啟數據源代理的AT模式
4.TCC模式(重點)
TCC模式原理
TCC模式與AT模式非常相似,每階段都是獨立事務,不同的是TCC通過人工編碼來實現數據恢復。需要實現三個方法:
-
Try:資源的檢測和預留;
-
Confirm:完成資源操作業務;要求 Try 成功 Confirm 一定要能成功。
-
Cancel:預留資源釋放,可以理解為try的反向操作。
TCC模式凍結機制與AT模式中的快照恢復機制有很大的相同點:
TCC凍結機制其實是相當于恢復操作的數據(例如金額),而AT模式快照恢復是恢復全部的數據(操作之前的數據),TCC實現了不需要全局鎖進行隔離取之代替的是凍結(相當于對操作的數據綁定了一個事務).TCC模式可以和AT模式混用
TCC的工作模型圖:
需要注意的是Confirm實現的不是提交具體的事務,提交具體的事務其實在資源預留已經做完了,Confirm實現的是刪除凍結記錄操作,相當于TC確認所有事務執行無誤,提交的全局事務
總結:
TCC的優點是什么?
-
一階段完成直接提交事務,釋放數據庫資源,性能好
-
相比AT模型,無需生成快照,無需使用全局鎖,性能最強
-
不依賴數據庫事務,而是依賴補償操作,可以用于非事務型數據庫
TCC的缺點是什么?
-
有代碼侵入,需要人為編寫try、Confirm和Cancel接口,太麻煩
-
軟狀態,事務是最終一致
-
需要考慮Confirm和Cancel的失敗情況,做好冪等處理
補充:
1.什么是冪等?
"冪等"是指對同一操作的多次執行具有相同的效果,不會導致不一致或意外的結果。換句話說,如果一個操作是冪等的,那么無論執行多少次,最終的狀態都是相同的。
2.在TCC中的空回滾和業務懸掛
空回滾:當某分支事務的try階段阻塞時,可能導致全局事務超時而觸發二階段的cancel操作。在未執行try操作時先執行了cancel操作,這時cancel不能做回滾,就是空回滾。
業務懸掛:對于已經空回滾的業務,如果以后繼續執行try,就永遠不可能confirm或cancel,這就是業務懸掛。應當阻止執行空回滾后的try操作,避免懸掛
簡單來說,空回滾就是在業務還沒執行之前就進行回滾了,業務懸掛是在空回滾后繼續執行了try邏輯
案例:改造account-service服務,利用TCC實現分布式事務
業務分析:
為了實現空回滾、防止業務懸掛,以及冪等性要求。我們必須在數據庫記錄凍結金額的同時,記錄當前事務id和執行狀態,為此我們設計了一張表:
業務邏輯:
實現方法:
1.聲明TCC接口
TCC的Try、Confirm、Cancel方法都需要在接口中基于注解來聲明,語法如下:
@LocalTCC
public interface AccountTCCService {/*** Try邏輯,@TwoPhaseBusinessAction中的name屬性要與當前方法名一致,用于指定Try邏輯對應的方法* @param userId* @param money*/@TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel")void deduct(@BusinessActionContextParameter("userId") String userId,@BusinessActionContextParameter("money") int money);/**** 二階段confirm確認方法、可以另命名,但要保證與commitMethod一致** @param context 上下文,可以傳遞try方法的參數* @return boolean 執行是否成功 **/boolean confirm(BusinessActionContext context);/*** 二階段回滾方法,要保證與rollbackMethod一致* @param context* @return*/boolean cancel(BusinessActionContext context);
}
2.導入凍結表
3.實現AccountTCCService接口
package cn.itcast.account.service.impl;import cn.itcast.account.entity.AccountFreeze;
import cn.itcast.account.mapper.AccountFreezeMapper;
import cn.itcast.account.mapper.AccountMapper;
import cn.itcast.account.service.AccountTCCService;
import io.seata.core.context.RootContext;
import io.seata.rm.tcc.api.BusinessActionContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;@Service
@Slf4j
public class AccountTCCServiceImpl implements AccountTCCService {@Autowiredprivate AccountMapper accountMapper;@Autowiredprivate AccountFreezeMapper accountFreezeMapper;@Override@Transactionalpublic void deduct(String userId, int money) {//獲取全局事務idString xid = RootContext.getXID();AccountFreeze oldFreeze = accountFreezeMapper.selectById(xid);if(oldFreeze != null){return;}accountMapper.deduct(userId, money);//記錄凍結的金額和事務AccountFreeze freeze = new AccountFreeze();freeze.setUserId(userId);freeze.setFreezeMoney(money);freeze.setState(AccountFreeze.State.TRY);freeze.setXid(xid);accountFreezeMapper.insert(freeze);}@Overridepublic boolean confirm(BusinessActionContext context) {String xid = context.getXid();int count = accountFreezeMapper.deleteById(xid);return count == 1;}@Overridepublic boolean cancel(BusinessActionContext context) {//查詢凍結記錄String xid = context.getXid();String userId = context.getActionContext("userId").toString();AccountFreeze freeze = accountFreezeMapper.selectById(xid);//空回滾判斷if (freeze == null) {freeze = new AccountFreeze();freeze.setUserId(userId);freeze.setFreezeMoney(0);freeze.setState(AccountFreeze.State.CANCEL);freeze.setXid(xid);accountFreezeMapper.insert(freeze);return true;}//冪等處理if(freeze.getState() == AccountFreeze.State.CANCEL){return true;}//恢復可用余額accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney());//將凍結的金額清零,狀態改為CANCELfreeze.setFreezeMoney(0);freeze.setState(AccountFreeze.State.CANCEL);int count = accountFreezeMapper.updateById(freeze);return count == 1;}
}
需要注意的是:
1.空回滾判斷需要在回滾業務中編寫,且需要將記錄插入到凍結表中,便于業務懸掛做出判斷
2.對于業務懸掛需要先查詢凍結表中是否有記錄,如果有,一定是CANCEL執行過(因為對于沒有回滾過的業務,在執行業務結束后對應的凍結表的字段一定為空,所以說有記錄一定為CANCEL執行過),對于CANCEL執行過的事務說明全局事務已經完成,就必須拒絕此刻的try操作,否則會引起業務懸掛
5.Saga模式(了解)
Saga模式是SEATA提供的長事務解決方案。也分為兩個階段:
-
一階段:直接提交本地事務
-
二階段:成功則什么都不做;失敗則通過編寫補償業務來回滾
Saga模式優點:
-
事務參與者可以基于事件驅動實現異步調用,吞吐高
-
一階段直接提交事務,無鎖,性能好
-
不用編寫TCC中的三個階段,實現簡單
缺點:
-
軟狀態持續時間不確定,時效性差
-
沒有鎖,沒有事務隔離,會有臟寫
四.四種模式對比
五.Seata高可用
什么是異地容災?
確保在發生災難性事件或緊急情況時,組織的業務能夠迅速恢復正常運行。異地容災的主要目標是最小化業務中斷,并確保在災難性事件后能夠迅速恢復關鍵業務功能。
TC的異地多機房容災架構
TC服務作為Seata的核心服務,一定要保證高可用和異地容災。
當一個地方的集群出現問題的時候,TC服務能快速切換到另外一個集群,故需要實現配置管理的熱更新,就需要nacos配置管理來實現了,實現方式主要是在nacos統一配置管理,然后在各個微服務讀取nacos中的properties文件即可(在每個微服務的yml文件指定nacos的地址和配置文件名稱即可)