分布式事務(wù)框架seata原理
Seata簡(jiǎn)介
Seata(原名Fescar) 是阿里18年開源的分布式事務(wù)的框架。Fescar的開源對(duì)分布式事務(wù)框架領(lǐng)域影響 很大。作為開源大戶幸逆,F(xiàn)escar來(lái)自阿里的GTS,經(jīng)歷了好幾次雙十一的考驗(yàn)晰房,一經(jīng)開源便頗受關(guān)注柠新。同時(shí)Fescar也保留了接近0業(yè)務(wù)入侵的優(yōu)點(diǎn)窍荧,只需要簡(jiǎn)單的配置Fescar的數(shù)據(jù)代理和加個(gè)注解,加一個(gè) Undolog表恨憎,就可以達(dá)到我們想要的目的蕊退。
Seata原理
seata將一個(gè)本地事務(wù)作為一個(gè)分布式事務(wù)的分支,若干個(gè)分支分布在不同的微服務(wù)上,組成一個(gè)全局事務(wù).seata包含三個(gè)結(jié)構(gòu):
Transaction Coordinator (TC): 事務(wù)協(xié)調(diào)器,維護(hù)全局事務(wù)的運(yùn)行狀態(tài) ,負(fù)責(zé)協(xié)調(diào)驅(qū)動(dòng)全局事務(wù)的提交或者回滾.
Transaction Manager (TM): 控制全局事務(wù)的邊界憔恳,負(fù)責(zé)開啟一個(gè)全局事務(wù)瓤荔,并最終發(fā)起全局提交或全局回滾的決議。
Resource Manager (RM): 控制分支事務(wù)钥组,負(fù)責(zé)分支注冊(cè)输硝、狀態(tài)匯報(bào),并接收事務(wù)協(xié)調(diào)器的指令程梦,驅(qū)動(dòng)分支(本地)事務(wù)的提交和回滾点把。
其中TM向TC申請(qǐng)一個(gè)全局事務(wù),并生成一個(gè)全局唯一的XID.XID會(huì)在全局事務(wù)調(diào)用分支事務(wù)時(shí)在調(diào)用鏈路的上下文中傳播,RM會(huì)攜帶XID向TC申請(qǐng)注冊(cè)分支事務(wù),TC調(diào)度XID下的所有分支事務(wù)的提交和回滾.
一個(gè)典型的分布式事務(wù)過(guò)程:
TM 向 TC 申請(qǐng)開啟一個(gè)全局事務(wù),全局事務(wù)創(chuàng)建成功并生成一個(gè)全局唯一的 XID屿附。
XID 在微服務(wù)調(diào)用鏈路的上下文中傳播郎逃。
RM 向 TC 注冊(cè)分支事務(wù),將其納入 XID 對(duì)應(yīng)全局事務(wù)的管轄,并會(huì)執(zhí)行分支事務(wù)并提交(RM在第一階段就已經(jīng)執(zhí)行了本地事務(wù)的提交或回滾),最后將執(zhí)行結(jié)果匯報(bào)給TC
TM根據(jù)TC中的分支事務(wù)執(zhí)行情況 向 TC 發(fā)起針對(duì) XID 的全局提交或回滾決議挺份。
TC 調(diào)度 XID 下管轄的全部分支事務(wù)完成提交或回滾請(qǐng)求衣厘。
Seata模式
seata提供了三種實(shí)現(xiàn)分布式事務(wù)的模式:AT模式,MT模式和混合模式
AT模式
業(yè)務(wù)邏輯不需要關(guān)注事務(wù)機(jī)制,分支與全局事務(wù)的交互過(guò)程自動(dòng)進(jìn)行。
AT****模式:主要關(guān)注多 DB 訪問(wèn)的數(shù)據(jù)一致性影暴,實(shí)現(xiàn)起來(lái)比較簡(jiǎn)單错邦,對(duì)業(yè)務(wù)的侵入較小。AT模式部分代碼如下:不需要關(guān)注執(zhí)行狀態(tài)型宙,對(duì)業(yè)務(wù)代碼侵入較小撬呢。類似代碼如下,只需要為方法添 加 @GlobalTransactional 注解即可妆兑。
AT模式的核心是對(duì)業(yè)務(wù)無(wú)侵入魂拦,是一種改進(jìn)后的兩階段提交.
詳細(xì)流程
第一階段
第一步:解析sql語(yǔ)句,得到 SQL 的類型(UPDATE)搁嗓,表(product)芯勘,條件(where name = 'TXC')等相關(guān)的信息。
第二步:查詢老數(shù)據(jù)腺逛,根據(jù)上面的where語(yǔ)句sql荷愕,去數(shù)據(jù)庫(kù)查詢?cè)嫉臄?shù)據(jù)。
如 select * from product where name = 'TXC';得到原始的數(shù)據(jù)棍矛,如該行id=1安疗,然后記錄下來(lái)。
第三步:執(zhí)行第一步的sql語(yǔ)句够委,即執(zhí)行update荐类,修改數(shù)據(jù)庫(kù)的該記錄的值。
第四步:查詢修改后的值茁帽,select * from product where id =1.得到該行值玉罐,記錄下來(lái)。
第五步:插入回滾日志潘拨,將老值吊输、新值以及sql語(yǔ)句組成一個(gè)將來(lái)可用于回滾的日志,插入到UNDO_LOG表战秋。
{
"branchId": 641789253,
"undoItems": [{
"afterImage": { //更新后的數(shù)據(jù)
"rows": [{
"fields": [{
"name": "id",
"type": 4,
"value": 1
}, {
"name": "name",
"type": 12,
"value": "GTS"
}, {
"name": "since",
"type": 12,
"value": "2014"
}]
}],
"tableName": "product" //數(shù)據(jù)表名
},
"beforeImage": { //更新前的數(shù)據(jù)
"rows": [{
"fields": [{
"name": "id",
"type": 4,
"value": 1
}, {
"name": "name",
"type": 12,
"value": "TXC"
}, {
"name": "since",
"type": 12,
"value": "2014"
}]
}],
"tableName": "product" //數(shù)據(jù)表名
},
"sqlType": "UPDATE" //sql語(yǔ)句類型
}],
"xid": "xid:xxx" //全局事務(wù)id
}
第六步:向TC server注冊(cè)分支璧亚,申請(qǐng)product表,id=1的行的全局鎖脂信。注意癣蟋,這個(gè)全局鎖是相對(duì)于所有可能的同時(shí)在執(zhí)行的分布式事務(wù)而言的。一旦某個(gè)分支狰闪,獲取了該記錄的全局鎖疯搅,在解鎖之前,任何其他的分布式事務(wù)埋泵,不能修改該數(shù)據(jù)幔欧。
第七步:本地事務(wù)提交罪治,將自己的本地事務(wù)、和前面的UNDO LOG一起提交礁蔗。
第八步:將本地事務(wù)提交的結(jié)果上報(bào)給TC server觉义。如成功、失敗浴井。
第二階段
成功的情況:分支收到了TC下發(fā)的成功請(qǐng)求晒骇,立馬返回我已OK的結(jié)果給TC,然后異步執(zhí)行刪除UNDO LOG的操作磺浙。因?yàn)槌晒α撕槎冢杂脕?lái)回滾的UNDO LOG就沒意義了,異步刪除掉就好撕氧。
失敗的情況:
1 分支收到了TC下發(fā)的失敗請(qǐng)求瘤缩,開始執(zhí)行回滾邏輯。
2 通過(guò) XID 和 Branch ID 查找到相應(yīng)的 UNDO LOG 記錄伦泥。
3 數(shù)據(jù)校驗(yàn):拿 UNDO LOG 中的后鏡與當(dāng)前數(shù)據(jù)進(jìn)行比較剥啤,如果有不同,說(shuō)明數(shù)據(jù)被當(dāng)前全局事務(wù)之外的動(dòng)作做了修改奄喂。這種情況铐殃,需要根據(jù)配置策略來(lái)做處理海洼,詳細(xì)的說(shuō)明在另外的文檔中介紹跨新。
4 根據(jù) UNDO LOG 中的前鏡像和業(yè)務(wù) SQL 的相關(guān)信息生成并執(zhí)行回滾的語(yǔ)句。
update product set name = 'TXC' where id = 1;
5 提交本地事務(wù)坏逢。并把本地事務(wù)的執(zhí)行結(jié)果(即分支事務(wù)回滾的結(jié)果)上報(bào)給 TC域帐。
結(jié)論:
可以看到,整體來(lái)說(shuō)是整,這個(gè)分布式事務(wù)是比較迅速的肖揣,在不等待全局鎖的情況下,基本和本地事務(wù)沒什么區(qū)別浮入×牛回滾時(shí),也不依賴數(shù)據(jù)庫(kù)本身的回滾能力事秀,都由自己業(yè)務(wù)來(lái)實(shí)現(xiàn)回滾操作彤断。
臟讀和臟寫
臟讀:在RM提交本地事務(wù)之后,TC提交全局事務(wù)之前,由于本地事務(wù)已提交,其他事務(wù)便可以讀取到已提交事務(wù)修改的數(shù)據(jù),但在TC回滾全局事務(wù)之后,其他事務(wù)讀取到的數(shù)據(jù)又會(huì)變?yōu)楦虑暗臄?shù)據(jù)
官方給出的解決方案為:臟讀取Select語(yǔ)句用于更新,代理方法使用@ GlobalLock + @ Transactional或@GlobalTransaction
臟寫:在TC通知分支事務(wù)回滾后,分支事務(wù)回滾之前,發(fā)現(xiàn)涉及的數(shù)據(jù)已經(jīng)被修改,無(wú)法和UNDO LOG記錄中的舊數(shù)據(jù)匹配上,則會(huì)回滾失敗!
官方給出的解決方案為: 臟寫您必須使用@globaltransaction注意:如果要查詢的業(yè)務(wù)接口不使用@globaltransactional批注易迹,這意味著該方法不需要分布式事務(wù)宰衙,則可以在該方法上批注@ globallock + @ Transactional批注方法,然后在查詢中添加for update語(yǔ)句睹欲。如果您的查詢接口在事務(wù)鏈接的外邊緣具有@globaltransactional批注供炼,則只需在查詢中添加for update語(yǔ)句即可一屋。設(shè)計(jì)此批注的原因是,在分布式注釋可用之前袋哼,分布式事務(wù)需要查詢已提交的數(shù)據(jù)冀墨,而業(yè)務(wù)則不需要分布式事務(wù)。使用GlobalTransactional批注會(huì)增加一些不必要的RPC額外開銷涛贯,例如開始返回xid轧苫,提交事務(wù)等。
@globaltransactional注解會(huì)開啟全局事務(wù)和本地事務(wù)
@ globallock 聲明事務(wù)僅執(zhí)行在本地RM中疫蔓,但是本次事務(wù)確保在更新狀態(tài)下的操作記錄不會(huì)被其他全局事務(wù)操作含懊。即將本地事務(wù)的執(zhí)行納入seata分布式事務(wù)的管理,一起競(jìng)爭(zhēng)全局鎖衅胀,保證全局事務(wù)在執(zhí)行的時(shí)候岔乔,本地業(yè)務(wù)不可以操作全局事務(wù)中的記錄。
@ globallock + @ Transactional注解只會(huì)開啟本地事務(wù)和獲取全局鎖
一階段本地事務(wù)提交前滚躯,需要確保先拿到 全局鎖 雏门。
拿不到 全局鎖 ,不能提交本地事務(wù)掸掏。
拿 全局鎖 的嘗試被限制在一定范圍內(nèi)茁影,超出范圍將放棄,并回滾本地事務(wù)丧凤,釋放本地鎖募闲。
舉例:
兩個(gè)全局事務(wù) tx1 和 tx2,分別對(duì) a 表的 m 字段進(jìn)行更新操作愿待,m 的初始值 1000浩螺。
tx1 先開始,開啟本地事務(wù)仍侥,拿到本地鎖要出,更新操作 m = 1000 - 100 = 900。本地事務(wù)提交前农渊,先拿到該記錄的 全局鎖 患蹂,本地提交釋放本地鎖。
tx2 后開始砸紊,開啟本地事務(wù)传于,拿到本地鎖,更新操作 m = 900 - 100 = 800批糟。本地事務(wù)提交前格了,嘗試拿該記錄的 全局鎖 ,tx1 全局提交前徽鼎,該記錄的全局鎖被 tx1 持有盛末,tx2 需要重試等待 全局鎖 弹惦。