上一篇文章,我們介紹了分布式事務(wù)的基本理論和幾種實(shí)現(xiàn)方式,現(xiàn)在,我們來(lái)了解下Seata。
Seata 是阿里開(kāi)源的基于Java的分布式事務(wù)解決方案,共提供了4種模式解決分布式事務(wù)場(chǎng)景,分別是AT,XA,TCC,Saga。其中XA,TCC,Saga咱們都介紹過(guò),現(xiàn)在來(lái)看下下AT。AT模式是阿里的 GTS(Seata 由 GTS 開(kāi)源而來(lái))所提出的一種事務(wù)模式,這是Seata的一大特色,AT對(duì)業(yè)務(wù)代碼完全無(wú)侵入性,使用非常簡(jiǎn)單,改造成本低。我們只需要關(guān)注自己的業(yè)務(wù)SQL,Seata會(huì)通過(guò)分析我們業(yè)務(wù)SQL,反向生成回滾數(shù)據(jù)。
AT包含兩個(gè)階段:
- 第一階段(業(yè)務(wù)執(zhí)行+記錄回滾日志)
業(yè)務(wù)操作提交時(shí),Seata 會(huì)自動(dòng)攔截 INSERT、UPDATE 等 SQL 操作,在執(zhí)行這些操作的同時(shí),Seata 會(huì)在業(yè)務(wù)數(shù)據(jù)庫(kù)中記錄回滾日志(Undo Log),用于恢復(fù)數(shù)據(jù)。 - 第二階段(提交/回滾事務(wù))
如果全局事務(wù)成功,Seata 自動(dòng)清理回滾日志;如果失敗,Seata 會(huì)根據(jù)回滾日志自動(dòng)恢復(fù)數(shù)據(jù)到操作前的狀態(tài)。
接下來(lái),我們來(lái)看Seata的模塊組成
1)TM:事務(wù)發(fā)起者。負(fù)責(zé)告知 TC,分布式事務(wù)的開(kāi)始,提交,回滾。
2)RM:資源管理者。管理各個(gè)分支事務(wù)的資源,每一個(gè) RM 都會(huì)作為一個(gè)分支事務(wù)注冊(cè)在 TC。
3)TC :事務(wù)協(xié)調(diào)者。負(fù)責(zé)我們的事務(wù)ID的生成,事務(wù)注冊(cè)、提交、回滾等。
本篇我們著重介紹Seata的AT模式
在Seata的AT模式中,TM和RM都作為SDK的一部分集成在業(yè)務(wù)系統(tǒng),我們可以認(rèn)為是Client端。TC是Server端。
工作流程
我們用一個(gè)比較簡(jiǎn)單的業(yè)務(wù)場(chǎng)景來(lái)描述一下Seata AT模式的工作過(guò)程。有個(gè)充值業(yè)務(wù),現(xiàn)在有兩個(gè)服務(wù),一個(gè)負(fù)責(zé)管理用戶的余額,另外一個(gè)負(fù)責(zé)管理用戶的積分。當(dāng)用戶充值的時(shí)候,首先增加用戶賬戶上的余額,然后增加用戶的積分。
AT流程分為兩階段,主要邏輯全部在第一階段,第二階段主要做回滾或日志清理的工作。其中,第一階段流程如下:

積分服務(wù)中也有TM,但是由于沒(méi)有用到,因此直接可以忽略。
1)余額服務(wù)中的TM,向TC申請(qǐng)開(kāi)啟一個(gè)全局事務(wù),TC會(huì)返回一個(gè)全局的事務(wù)ID。
2)余額服務(wù)在執(zhí)行本地業(yè)務(wù)之前,RM會(huì)先向TC注冊(cè)分支事務(wù)。
3)余額服務(wù)依次生成undo log、執(zhí)行本地事務(wù)、生成redo log,最后直接提交本地事務(wù)。
4)余額服務(wù)的RM向TC匯報(bào),事務(wù)狀態(tài)是成功的。
5)余額服務(wù)發(fā)起遠(yuǎn)程調(diào)用,把事務(wù)ID傳給積分服務(wù)。
6)積分服務(wù)在執(zhí)行本地業(yè)務(wù)之前,也會(huì)先向TC注冊(cè)分支事務(wù)。
7)積分服務(wù)次生成undo log、執(zhí)行本地事務(wù)、生成redo log,最后直接提交本地事務(wù)。
8)積分服務(wù)的RM向TC匯報(bào),事務(wù)狀態(tài)是成功的。
9)積分服務(wù)返回遠(yuǎn)程調(diào)用成功給余額服務(wù)。
10)余額服務(wù)的TM向TC申請(qǐng)全局事務(wù)的提交/回滾。
第二階段的邏輯就比較簡(jiǎn)單了。Client和TC之間是有長(zhǎng)連接的,如果是正常全局提交,則TC通知多個(gè)RM異步清理掉本地的redo和undo log即可。如果是回滾,則TC通知每個(gè)RM回滾數(shù)據(jù)即可。
至此我們已經(jīng)初步了解了Seata的AT模式是如何實(shí)現(xiàn)的了,如果你也和我一樣,仔細(xì)思考了上述過(guò)程,可能會(huì)提出一些問(wèn)題,這邊我列舉一下我在學(xué)習(xí)Seata時(shí),遇到的問(wèn)題,以及我得出的結(jié)論。
問(wèn)題1. Seata如何做到無(wú)侵入的分析業(yè)務(wù)SQL生成undoLog,注冊(cè)事務(wù)分支等操作?
Seata 代理了DataSource,我們可以通過(guò)在代碼注入一個(gè)DataSource來(lái)驗(yàn)證我的說(shuō)法,目前的DataSource 是 io.seata.rm.datasource.DataSourceProxy

所有的Java持久化框架,最終在操作數(shù)據(jù)庫(kù)時(shí)都會(huì)通過(guò)DataSource接口獲取Connection,通過(guò)Connection 實(shí)現(xiàn)對(duì)數(shù)據(jù)庫(kù)的增刪改查,事務(wù)控制。

Seata 通過(guò)代理的Connection做到了無(wú)侵入的生成undoLog,注冊(cè)事務(wù)分支,具體源碼可以查看io.seata.rm.datasource.ConnectionProxy
問(wèn)題2. ConnectionProxy 如何判斷當(dāng)前事務(wù)是全局事務(wù),還是本地事務(wù)?
通過(guò)當(dāng)前線程是否綁定了全局事務(wù)id,在進(jìn)行全局事務(wù)之前,需要調(diào)用RootContext.bind(xid);
問(wèn)題3. 全局事務(wù)并發(fā)更新
Seata的寫隔離級(jí)別是全局獨(dú)占的,事務(wù)開(kāi)啟之前,TM會(huì)在TC中獲取全局鎖(用select for update嘗試,如果出現(xiàn)鎖沖突,那么不斷進(jìn)行重試,直到占本地鎖,而后獲取全局鎖)。鎖的的key會(huì)以行數(shù)據(jù)的維度來(lái)確定,即同一個(gè)數(shù)據(jù)庫(kù)中的某個(gè)表中的某行數(shù)據(jù),在同一時(shí)間只會(huì)被一個(gè)事務(wù)操作。
而為了保證高效性,讀的隔離級(jí)別是Read Uncommitted,當(dāng)全局事務(wù)未提交,而本地?cái)?shù)據(jù)提交時(shí),對(duì)其他全局事務(wù)是可見(jiàn)的,不過(guò)也沒(méi)關(guān)系,由于全局鎖的限制,其他全局事務(wù)不能操作該條數(shù)據(jù),必須等當(dāng)前全局事務(wù)提交。
舉個(gè)例子,產(chǎn)品份額有 5W,A 用戶買了 2W,份額Branch一階段完畢(本地事務(wù)份額已經(jīng)扣除 Commit),但是在下單的時(shí)候異常了。因?yàn)楸镜厥聞?wù)讀已提交,這時(shí)候 Seata 允許業(yè)務(wù)訪問(wèn)該條數(shù)據(jù) 3W,在 A 用戶的份額Branch未回滾成功前,對(duì)其他用戶可見(jiàn),但其他用戶并不能購(gòu)買該產(chǎn)品,必須等到產(chǎn)品份額回滾到 5W,其他用戶才可以操作產(chǎn)品數(shù)據(jù)。
當(dāng)然,如果應(yīng)用一定要達(dá)到Read Committed級(jí)別,可以在sql中使用SELECT FOR UPDATE 語(yǔ)句,Seata會(huì)鎖定持有數(shù)據(jù)的行鎖,直到全局鎖是已提交的,才返回。
問(wèn)題4. 全局事務(wù)外的更新
有些全局事務(wù)外的方法,它可能并不需要@GlobalTransactional的事務(wù)管理,但是我們又希望它對(duì)數(shù)據(jù)的修改能夠加入到seata機(jī)制當(dāng)中。那么這時(shí)候就需要@GlobalLock了。加上了@GlobalLock,在事務(wù)提交的時(shí)候就會(huì)去checkLock校驗(yàn)一下全局鎖。
問(wèn)題5. @GlobalTransactional 和 @Transactional 同時(shí)使用會(huì)怎么樣
@GlobalTransactional他是負(fù)責(zé)開(kāi)啟全局事務(wù)/提交事務(wù)1階段,說(shuō)白了@GlobalTransactional 只和Seata-server 交互,而 @Transactional 管理的是本地?cái)?shù)據(jù)庫(kù)的事務(wù),所以二者不發(fā)生沖突。
問(wèn)題6. 如果其中某一個(gè)事務(wù)分支超時(shí)未提交,會(huì)發(fā)生什么
Seata的全局事務(wù)超時(shí)時(shí)間,默認(rèn)是1分鐘,Seata-server 在檢測(cè)到有超時(shí)的全局事務(wù)時(shí),會(huì)向所有已提交的分支,發(fā)起回滾。而超時(shí)提交的事務(wù),向Seata-server發(fā)起分支注冊(cè)時(shí),響應(yīng)結(jié)果為事務(wù)已超時(shí),或者事務(wù)不存在,也會(huì)回滾本地事務(wù)。
問(wèn)題7. Seata-client 如何接收Seata-server發(fā)起的通知
Seata-client 包含了Netty服務(wù),在啟動(dòng)時(shí)Netty會(huì)監(jiān)聽(tīng)端口,并向Seata-server 發(fā)起注冊(cè)。server中存儲(chǔ)了client 的調(diào)用地址。
與TCC模式性能對(duì)比
使用AT 模式時(shí),當(dāng)事務(wù)需要回滾,會(huì)根據(jù) Undo Log 進(jìn)行恢復(fù),而恢復(fù)過(guò)程可能涉及大量的反向操作,特別是在數(shù)據(jù)量大或并發(fā)高的情況下,性能瓶頸比較明顯。而且AT 模式依賴數(shù)據(jù)庫(kù)的鎖機(jī)制來(lái)保持?jǐn)?shù)據(jù)一致性,因此在某些高并發(fā)場(chǎng)景下,數(shù)據(jù)庫(kù)可能會(huì)出現(xiàn)較大的鎖沖突,導(dǎo)致性能下降。
使用TCC模式時(shí),在高并發(fā)場(chǎng)景下,Try 階段的資源預(yù)留操作可能導(dǎo)致較多的鎖定和資源占用,增加系統(tǒng)負(fù)擔(dān)。
如果你對(duì)性能要求極高,并且可以接受較高的開(kāi)發(fā)復(fù)雜性,TCC 模式可能是更好的選擇;而如果你希望降低開(kāi)發(fā)成本,且業(yè)務(wù)并發(fā)量較低,AT 模式更適合。
總結(jié)
對(duì)比起TCC等其他事務(wù)模型,Seata的AT模式適合大部分業(yè)務(wù)場(chǎng)景,開(kāi)發(fā)簡(jiǎn)單。
整個(gè)事務(wù)的協(xié)調(diào)、提交或回滾操作,都可以通過(guò)AOP完成,開(kāi)發(fā)者只需要關(guān)注業(yè)務(wù)即可。
但是在高并發(fā)情況下,數(shù)據(jù)庫(kù)鎖可能成為瓶頸,尤其是回滾時(shí)的性能較差。
引用:
https://www.pianshen.com/article/55481052910/
http://seata.io/zh-cn/blog/seata-at-tcc-saga.html
http://www.itdecent.cn/p/e5dc4d07e24e