目錄
Seata簡介
Seata(Simple Extensible Autonomous Transaction Architecture) 是 阿里巴巴開源的分佈式事務中間件,以高效並且對業務 0 侵入的方式,解決微服務場景下面臨的分佈式事務問題。
附上項目github鏈接
目前Seata還處於不斷開源升級中,並不建議在線上使用,生產環境可以考慮使用阿里雲商用的GTS,附上Seata目前的升級計劃,可以考慮在V1.0,即服務端HA集羣版本進行線上使用
先來看下爲什麼會產生分佈式事務問題
分佈式事務產生背景
講到事務,又得搬出經典的銀行轉賬問題了,下面以實例說明
假設銀行(bank)中有兩個客戶(name)張三和李四
我們需要將張三的1000元存款(sal)轉到李四的賬戶上
目標就是張三賬戶減1000,李四賬戶加1000,不能出現中間步驟(張三減1000,李四沒加)
假設dao層代碼如下
public interface BankMapper {
/**
* @param userName 用戶名
* @param changeSal 餘額變動值
*/
public void updateSal(String userName,int changeSal);
}
對應xml中sql如下
<update id="updateSal">
update bank SET sal = sal+#{changeSal} WHERE name = #{userName}
</update>
如果兩個用戶對應的銀行存款數據在一個數據源中,即一個數據庫中,那麼service層代碼可以如下編寫
/**
* @param fromUserName 轉賬人
* @param toUserName 被轉賬人
* @param changeSal 轉賬額度
*/
@Transactional(rollbackFor = Exception.class)
public void changeSal(String fromUserName,String toUserName,int changeSal) {
bankMapper.updateSal(fromUserName, -1 * changeSal);
bankMapper.updateSal(toUserName, changeSal);
}
通過spring框架下的@Transactional註解來保證單一數據源增刪改查的一致性
但是隨着業務的不斷擴大,用戶數在不斷變多,幾百萬幾千萬用戶時數據可以存一個庫甚至一個表裏,假設有10個億的用戶?
數據庫的水平分割
爲了解決數據庫上的瓶頸,分庫是很常見的解決方案,不同用戶就可能落在不同的數據庫裏,原來一個庫裏的事務操作,現在變成了跨數據庫的事務操作。
此時@Transactional註解就失效了,這就是跨數據庫分佈式事務問題
微服務化
當然,更多的情形是隨着業務不斷增長,將業務中不同模塊服務拆分成微服務後,同時調用多個微服務所產生的
微服務化的銀行轉賬情景往往是這樣的
- 調用交易系統服務創建交易訂單;
- 調用支付系統記錄支付明細;
- 調用賬務系統執行 A 扣錢;
- 調用賬務系統執行 B 加錢;
如圖所示,每個系統都對應一個獨立的數據源,且可能位於不同機房,同時調用多個系統的服務很難保證同時成功,這就是跨服務分佈式事務問題
分佈式事務理論基礎
兩階段提交(2pc)
兩階段提交協議(Two Phase Commitment Protocol)中,涉及到兩種角色
一個事務協調者(coordinator):負責協調多個參與者進行事務投票及提交(回滾)
多個事務參與者(participants):即本地事務執行者
總共處理步驟有兩個
(1)投票階段(voting phase):協調者將通知事務參與者準備提交或取消事務,然後進入表決過程。參與者將告知協調者自己的決策:同意(事務參與者本地事務執行成功,但未提交)或取消(本地事務執行故障);
(2)提交階段(commit phase):收到參與者的通知後,協調者再向參與者發出通知,根據反饋情況決定各參與者是否要提交還是回滾;
如果所示 1-2爲第一階段,2-3爲第二階段
如果任一資源管理器在第一階段返回準備失敗,那麼事務管理器會要求所有資源管理器在第二階段執行回滾操作。通過事務管理器的兩階段協調,最終所有資源管理器要麼全部提交,要麼全部回滾,最終狀態都是一致的
圖片來自螞蟻金服公衆號
TCC
基本原理
TCC 將事務提交分爲 Try - Confirm - Cancel 3個操作。其和兩階段提交有點類似,Try爲第一階段,Confirm - Cancel爲第二階段,是一種應用層面侵入業務的兩階段提交。
操作方法 含義
Try 預留業務資源/數據效驗
Confirm 確認執行業務操作,實際提交數據,不做任何業務檢查,try成功,confirm必定成功,需保證冪等
Cancel 取消執行業務操作,實際回滾數據,需保證冪等
其核心在於將業務分爲兩個操作步驟完成。不依賴 RM 對分佈式事務的支持,而是通過對業務邏輯的分解來實現分佈式事務。
下面還是以銀行轉賬例子來說明
假設用戶user表中有兩個字段:可用餘額(available_money)、凍結餘額(frozen_money)
A扣錢對應服務A(ServiceA)
B加錢對應服務B(ServiceB)
轉賬訂單服務(OrderService)
業務轉賬方法服務(BusinessService)
ServiceA,ServiceB,OrderService都需分別實現try(),confirm(),cancle()方法,方法對應業務邏輯如下
ServiceA ServiceB OrderService
try() 校驗餘額(併發控制)
凍結餘額+1000
餘額-1000 凍結餘額+1000 創建轉賬訂單,狀態待轉賬
confirm() 凍結餘額-1000 餘額+1000
凍結餘額-1000 狀態變爲轉賬成功
cancle() 凍結餘額-1000
餘額+1000 凍結餘額-1000 狀態變爲轉賬失敗
其中業務調用方BusinessService中就需要調用
ServiceA.try()
ServiceB.try()
OrderService.try()
1、當所有try()方法均執行成功時,對全局事物進行提交,即由事物管理器調用每個微服務的confirm()方法
2、 當任意一個方法try()失敗(預留資源不足,抑或網絡異常,代碼異常等任何異常),由事物管理器調用每個微服務的cancle()方法對全局事務進行回滾
引用網上一張TCC原理的參考圖片
TCC-冪等控制
使用TCC時要注意Try - Confirm - Cancel 3個操作的冪等控制,網絡原因,或者重試操作都有可能導致這幾個操作的重複執行
業務實現過程中需重點關注冪等實現,講到冪等,以上述TCC轉賬例子中confirm()方法來說明
在confirm()方法中
餘額-1000,凍結餘額-1000,這一步是實現冪等性的關鍵,你會怎麼做?
大家在自己系統裏操作資金賬戶時,爲了防止併發情況下數據不一致的出現,肯定會避免出現這種代碼
//根據userId查到賬戶
Account account = accountMapper.selectById(userId);
//取出當前資金
int availableMoney = account.getAvailableMoney();
account.setAvailableMoney(availableMoney-1000);
//更新剩餘資金
accountMapper.update(account);
因爲這本質上是一個 讀-改-寫的過程,不是原子的,在併發情況下會出現數據不一致問題
所以最簡單的做法是
update account set available_money = available_money-1000 where user_id=#{userId}
這利用了數據庫行鎖特性解決了併發情況下的數據不一致問題,但是TCC中,單純使用這個方法適用麼?
答案是不行的,該方法能解決併發單次操作下的扣減餘額問題,但是不能解決多次操作帶來的多次扣減問題,假設我執行了兩次,按這種方案,用戶賬戶就少了2000塊
那麼具體怎麼做?上訴轉賬例子中,可以引入轉賬訂單狀態來做判斷,若訂單狀態爲已支付,則直接return
if( order!=null && order.getStatus().equals("轉賬成功")){
return;
}
當然,新建一張去重表,用訂單id做唯一建,若插入報錯返回也是可以的,不管怎麼樣,核心就是保證,操作冪等性
TCC-空回滾
如下圖所示,事務協調器在調用TCC服務的一階段Try操作時,可能會出現因爲丟包而導致的網絡超時,此時事務協調器會觸發二階段回滾,調用TCC服務的Cancel操作;
TCC服務在未收到Try請求的情況下收到Cancel請求,這種場景被稱爲空回滾;TCC服務在實現時應當允許空回滾的執行;
那麼具體代碼裏怎麼做呢?
分析下,如果try()方法沒執行,那麼訂單一定沒創建,所以cancle方法裏可以加一個判斷,如果上下文中訂單編號orderNo不存在或者訂單不存在,直接return
if(orderNo==null || order==null){
return;
}
核心思想就是 回滾請求處理時,如果對應的具體業務數據爲空,則返回成功
當然這種問題也可以通過中間件層面來實現,如,在第一階段try()執行完後,向一張事務表中插入一條數據(包含事務id,分支id),cancle()執行時,判斷如果沒有事務記錄則直接返回,但是現在還不支持
TCC-防懸掛
如下圖所示,事務協調器在調用TCC服務的一階段Try操作時,可能會出現因網絡擁堵而導致的超時,此時事務協調器會觸發二階段回滾,調用TCC服務的Cancel操作;在此之後,擁堵在網絡上的一階段Try數據包被TCC服務收到,出現了二階段Cancel請求比一階段Try請求先執行的情況;
用戶在實現TCC服務時,應當允許空回滾,但是要拒絕執行空回滾之後到來的一階段Try請求;
這裏又怎麼做呢?
可以在二階段執行時插入一條事務控制記錄,狀態爲已回滾,這樣當一階段執行時,先讀取該記錄,如果記錄存在,就認爲二階段回滾操作已經執行,不再執行try方法;
事務消息
事務消息更傾向於達成分佈式事務的最終一致性,適用於分佈式事務的提交或回滾只取決於事務發起方的業務需求,如A給B打了款並且成功了,那麼下游業務B一定需要加錢這種場景,或許下了單,用戶積分一定得增加這種場景。RocketMQ4.3中已經開源了事務消息,具體設計思路分析及demo演示,大家有興趣可以看下我寫的這篇文章
優缺點比較
事務方案 | 優點 | 缺點 |
2PC | 實現簡單 | 1、需要數據庫(一般是XA支持) 2、鎖粒度大,性能差 |
TCC | 鎖粒度小,性能好 | 需要侵入業務,實現較爲複雜,複雜業務實現冪等有難度 |
消息事務 | 業務侵入小,無需編寫業務回滾補償邏輯 | 事務消息實現難度大,強依賴第三方中間件可靠 |
Seata解決方案
解決分佈式事務問題,有兩個設計初衷
- 對業務無侵入:即減少技術架構上的微服務化所帶來的分佈式事務問題對業務的侵入
- 高性能:減少分佈式事務解決方案所帶來的性能消耗
seata中有兩種分佈式事務實現方案,AT及TCC
- AT模式主要關注多 DB 訪問的數據一致性,當然也包括多服務下的多 DB 數據訪問一致性問題
- TCC 模式主要關注業務拆分,在按照業務橫向擴展資源時,解決微服務間調用的一致性問題
AT模式(業務侵入小)
Seata AT模式是基於XA事務演進而來的一個分佈式事務中間件,XA是一個基於數據庫實現的分佈式事務協議,本質上和兩階段提交一樣,需要數據庫支持,Mysql5.6以上版本支持XA協議,其他數據庫如Oracle,DB2也實現了XA接口
角色如下
- Transaction Coordinator (TC): 事務協調器,維護全局事務的運行狀態,負責協調並驅動全局事務的提交或回滾
- Transaction Manager ™: 控制全局事務的邊界,負責開啓一個全局事務,並最終發起全局提交或全局回滾的決議
- Resource Manager (RM): 控制分支事務,負責分支註冊、狀態彙報,並接收事務協調器的指令,驅動分支(本地)事務的提交和回滾
基本處理邏輯如下
Branch就是指的分佈式事務中每個獨立的本地局部事務
第一階段
Seata 的 JDBC 數據源代理通過對業務 SQL 的解析,把業務數據在更新前後的數據鏡像組織成回滾日誌,利用 本地事務 的 ACID 特性,將業務數據的更新和回滾日誌的寫入在同一個 本地事務 中提交。
這樣,可以保證:任何提交的業務數據的更新一定有相應的回滾日誌存在
基於這樣的機制,分支的本地事務便可以在全局事務的第一階段提交,並馬上釋放本地事務鎖定的資源
這也是Seata和XA事務的不同之處,兩階段提交往往對資源的鎖定需要持續到第二階段實際的提交或者回滾操作,而有了回滾日誌之後,可以在第一階段釋放對資源的鎖定,降低了鎖範圍,提高效率,即使第二階段發生異常需要回滾,只需找對undolog中對應數據並反解析成sql來達到回滾目的
同時Seata通過代理數據源將業務sql的執行解析成undolog來與業務數據的更新同時入庫,達到了對業務無侵入的效果
第二階段
如果決議是全局提交,此時分支事務此時已經完成提交,不需要同步協調處理(只需要異步清理回滾日誌),Phase2 可以非常快速地完成
如果決議是全局回滾,RM 收到協調器發來的回滾請求,通過 XID 和 Branch ID 找到相應的回滾日誌記錄,通過回滾記錄生成反向的更新 SQL 並執行,以完成分支的回滾
TCC(高性能)
seata也針對TCC做了適配兼容,支持TCC事務方案,原理前面已經介紹過,基本思路就是使用侵入業務上的補償及事務管理器的協調來達到全局事務的一起提交及回滾,詳情參考demo回滾