轉(zhuǎn)自金融及分布式架構(gòu)公眾號
2019 年 3 月,螞蟻金服加入分布式事務(wù) Seata 的社區(qū)共建中感凤,并貢獻(xiàn)其 TCC 模式廷臼。本期是 SOFAChannel 第四期改含,主題:分布式事務(wù) Seata TCC 模式深度解析,本文根據(jù)覺生的直播整理民泵。
大家晚上好癣丧,我是 Seata Committer 覺生,來自螞蟻金服數(shù)據(jù)中間件團隊栈妆。今天的內(nèi)容主要分為以下四個部分:
· Seata TCC 模式的原理解析胁编;
· 從 TCC 的業(yè)務(wù)模型與并發(fā)控制分享如何設(shè)計一個 TCC 接口厢钧,并且適配 TCC 模型;
· 如何控制異常嬉橙;
· 性能優(yōu)化早直,使得 TCC 模式能夠滿足更高的業(yè)務(wù)需求。
1?Seata 的 TCC 模式
1.1 服務(wù)化拆分
下面我們就進(jìn)入第一個主題市框,Seata 的 TCC 模式霞扬。螞蟻金服早期是單系統(tǒng)架構(gòu),所有業(yè)務(wù)服務(wù)幾乎都在少數(shù)幾個系統(tǒng)中枫振。隨著業(yè)務(wù)的發(fā)展喻圃,業(yè)務(wù)越來越復(fù)雜,服務(wù)之間的耦合度也越來越高粪滤,故我們對系統(tǒng)進(jìn)行了重構(gòu)斧拍,服務(wù)按照功能進(jìn)行解耦和垂直拆分。拆分之后所帶來的問題就是一個業(yè)務(wù)活動原來只需要調(diào)用一個服務(wù)就能完成杖小,現(xiàn)在需要調(diào)用多個服務(wù)才能完成肆汹,而網(wǎng)絡(luò)、機器等不可靠窍侧,數(shù)據(jù)一致性的問題很容易出現(xiàn)县踢,與可擴展性、高可用容災(zāi)等要求并肩成為金融 IT 架構(gòu)支撐業(yè)務(wù)轉(zhuǎn)型升級的最大挑戰(zhàn)之一伟件。
從圖中可以看到硼啤,從單系統(tǒng)到微服務(wù)轉(zhuǎn)變,其實是一個資源橫向擴展的過程斧账,資源的橫向擴展是指當(dāng)單臺機器達(dá)到資源性能瓶頸谴返,無法滿足業(yè)務(wù)增長需求時,就需要橫向擴展資源咧织,形成集群嗓袱。通過橫向擴展資源,提升非熱點數(shù)據(jù)的并發(fā)性能习绢,這對于大體量的互聯(lián)網(wǎng)產(chǎn)品來說渠抹,是至關(guān)重要的。服務(wù)的拆分闪萄,也可以認(rèn)為是資源的橫向擴展梧却,只不過方向不同而已。
資源橫向擴展可能沿著兩個方向發(fā)展败去,包括業(yè)務(wù)拆分和數(shù)據(jù)分片:
業(yè)務(wù)拆分放航。根據(jù)功能對數(shù)據(jù)進(jìn)行分組,并將不同的微服務(wù)分布在多個不同的數(shù)據(jù)庫上圆裕,這實際上就是 SOA 架構(gòu)下的服務(wù)化广鳍。業(yè)務(wù)拆分就是把業(yè)務(wù)邏輯從一個單系統(tǒng)拆分到多個微服務(wù)中荆几。
數(shù)據(jù)分片。在微服務(wù)內(nèi)部將數(shù)據(jù)拆分到多個數(shù)據(jù)庫上赊时,為橫向擴展增加一個新的維度吨铸。數(shù)據(jù)分片就是把一個微服務(wù)下的單個 DB 拆分成多個 DB,具備一個 Sharding 的功能蛋叼。通過這樣的拆解焊傅,相當(dāng)于一種資源的橫向擴展,從而使得整個架構(gòu)可以承載更高的吞吐狈涮。
橫向擴展的兩種方法可以同時進(jìn)行運用:交易狐胎、支付與賬務(wù)三個不同微服務(wù)可以存儲在不同的數(shù)據(jù)庫中。另外歌馍,每個微服務(wù)內(nèi)根據(jù)其業(yè)務(wù)量可以再拆分到多個數(shù)據(jù)庫中握巢,各微服務(wù)可以相互獨立地進(jìn)行擴展。
Seata 關(guān)注的就是微服務(wù)架構(gòu)下的數(shù)據(jù)一致性問題松却,是一整套的分布式事務(wù)解決方案暴浦。Seata 框架包含兩種模式,一種是 AT 模式晓锻。AT 模式主要從數(shù)據(jù)分片的角度歌焦,關(guān)注多 DB 訪問的數(shù)據(jù)一致性,當(dāng)然也包括多服務(wù)下的多 DB 數(shù)據(jù)訪問一致性問題砚哆。
另外一個就是 TCC 模式独撇,TCC 模式主要關(guān)注業(yè)務(wù)拆分,在按照業(yè)務(wù)橫向擴展資源時躁锁,解決微服務(wù)間調(diào)用的一致性問題纷铣,保證讀資源訪問的事務(wù)屬性。
今天我們主要講的就是TCC模式战转。在講 TCC 之前搜立,我們先回顧一下 AT 模式,這樣有助于我們理解后面的 TCC 模式槐秧。
1.2 AT 模式
對于 AT 模式啄踊,之前其他同學(xué)已經(jīng)分享過很多次,大家也應(yīng)該比較熟悉了刁标。AT 模式下颠通,把每個數(shù)據(jù)庫被當(dāng)做是一個 Resource,Seata 里稱為 DataSource Resource命雀。業(yè)務(wù)通過 JDBC 標(biāo)準(zhǔn)接口訪問數(shù)據(jù)庫資源時蒜哀,Seata 框架會對所有請求進(jìn)行攔截斩箫,做一些操作吏砂。每個本地事務(wù)提交時撵儿,Seata RM(Resource Manager,資源管理器) 都會向 TC(Transaction Coordinator狐血,事務(wù)協(xié)調(diào)器) 注冊一個分支事務(wù)淀歇。當(dāng)請求鏈路調(diào)用完成后,發(fā)起方通知 TC 提交或回滾分布式事務(wù)匈织,進(jìn)入二階段調(diào)用流程浪默。此時,TC 會根據(jù)之前注冊的分支事務(wù)回調(diào)到對應(yīng)參與者去執(zhí)行對應(yīng)資源的第二階段缀匕。TC 是怎么找到分支事務(wù)與資源的對應(yīng)關(guān)系呢纳决?每個資源都有一個全局唯一的資源 ID,并且在初始化時用該 ID 向 TC 注冊資源乡小。在運行時阔加,每個分支事務(wù)的注冊都會帶上其資源 ID。這樣 TC 就能在二階段調(diào)用時正確找到對應(yīng)的資源满钟。
這就是我們的 AT 模式胜榔。簡單總結(jié)一下,就是把每個數(shù)據(jù)庫當(dāng)做一個 Resource湃番,在本地事務(wù)提交時會去注冊一個分支事務(wù)夭织。
1.3 TCC 模式
那么對應(yīng)到 TCC 模式里,也是一樣的吠撮,Seata 框架把每組 TCC 接口當(dāng)做一個 Resource尊惰,稱為 TCC?Resource。這套 TCC 接口可以是 RPC纬向,也以是服務(wù)內(nèi) JVM 調(diào)用择浊。在業(yè)務(wù)啟動時,Seata 框架會自動掃描識別到 TCC 接口的調(diào)用方和發(fā)布方逾条。如果是 RPC 的話琢岩,就是 sofa:reference、sofa:service师脂、dubbo:reference担孔、dubbo:service 等。
掃描到 TCC 接口的調(diào)用方和發(fā)布方之后吃警。如果是發(fā)布方糕篇,會在業(yè)務(wù)啟動時向 TC 注冊 TCC Resource,與 DataSource Resource 一樣酌心,每個資源也會帶有一個資源 ID拌消。
如果是調(diào)用方,Seata 框架會給調(diào)用方加上切面安券,與 AT 模式一樣墩崩,在運行時氓英,該切面會攔截所有對 TCC 接口的調(diào)用。每調(diào)用一次 Try 接口鹦筹,切面會先向 TC 注冊一個分支事務(wù)铝阐,然后才去執(zhí)行原來的 RPC 調(diào)用。當(dāng)請求鏈路調(diào)用完成后铐拐,TC 通過分支事務(wù)的資源 ID 回調(diào)到正確的參與者去執(zhí)行對應(yīng) TCC 資源的 Confirm 或 Cancel 方法徘键。
在講完了整個框架模型以后,大家可能會問 TCC 三個接口怎么實現(xiàn)遍蟋。因為框架本身很簡單吹害,主要是掃描 TCC 接口,注冊資源虚青,攔截接口調(diào)用赠制,注冊分支事務(wù),最后回調(diào)二階段接口挟憔。最核心的實際上是 TCC 接口的實現(xiàn)邏輯钟些。下面我將根據(jù)螞蟻金服內(nèi)部多年的實踐來為大家分析怎么實現(xiàn)一個完備的 TCC 接口。
2?TCC 業(yè)務(wù)模式與并發(fā)控制
2.1 TCC 設(shè)計原則
從 TCC 模型的框架可以發(fā)現(xiàn)绊谭,TCC 模型的核心在于 TCC 接口的設(shè)計政恍。用戶在接入 TCC 時,大部分工作都集中在如何實現(xiàn) TCC 服務(wù)上达传。下面我會分享螞蟻金服內(nèi)多年的 TCC 應(yīng)用實踐以及在 TCC 設(shè)計和實現(xiàn)過程中的注意事項篙耗。
設(shè)計一套 TCC 接口最重要的是什么?主要有兩點宪赶,第一點宗弯,需要將操作分成兩階段完成。TCC(Try-Confirm-Cancel)分布式事務(wù)模型相對于 XA 等傳統(tǒng)模型搂妻,其特征在于它不依賴 RM 對分布式事務(wù)的支持蒙保,而是通過對業(yè)務(wù)邏輯的分解來實現(xiàn)分布式事務(wù)。
TCC 模型認(rèn)為對于業(yè)務(wù)系統(tǒng)中一個特定的業(yè)務(wù)邏輯 欲主,其對外提供服務(wù)時邓厕,必須接受一些不確定性,即對業(yè)務(wù)邏輯初步操作的調(diào)用僅是一個臨時性操作扁瓢,調(diào)用它的主業(yè)務(wù)服務(wù)保留了后續(xù)的取消權(quán)详恼。如果主業(yè)務(wù)服務(wù)認(rèn)為全局事務(wù)應(yīng)該回滾,它會要求取消之前的臨時性操作引几,這就對應(yīng)從業(yè)務(wù)服務(wù)的取消操作昧互。而當(dāng)主業(yè)務(wù)服務(wù)認(rèn)為全局事務(wù)應(yīng)該提交時,它會放棄之前臨時性操作的取消權(quán),這對應(yīng)從業(yè)務(wù)服務(wù)的確認(rèn)操作敞掘。每一個初步操作屿储,最終都會被確認(rèn)或取消。因此渐逃,針對一個具體的業(yè)務(wù)服務(wù),TCC 分布式事務(wù)模型需要業(yè)務(wù)系統(tǒng)提供三段業(yè)務(wù)邏輯:
1.初步操作 Try:完成所有業(yè)務(wù)檢查民褂,預(yù)留必須的業(yè)務(wù)資源茄菊。
2.確認(rèn)操作 Confirm:真正執(zhí)行的業(yè)務(wù)邏輯,不做任何業(yè)務(wù)檢查赊堪,只使用 Try 階段預(yù)留的業(yè)務(wù)資源面殖。因此,只要 Try 操作成功哭廉,Confirm 必須能成功脊僚。另外,Confirm 操作需滿足冪等性遵绰,保證一筆分布式事務(wù)能且只能成功一次辽幌。
3.取消操作 Cancel:釋放 Try 階段預(yù)留的業(yè)務(wù)資源。同樣的椿访,Cancel 操作也需要滿足冪等性乌企。
第二點,就是要根據(jù)自身的業(yè)務(wù)模型控制并發(fā)成玫,這個對應(yīng) ACID 中的隔離性加酵。后面會詳細(xì)講到。
2.2 賬務(wù)系統(tǒng)模型設(shè)計
下面我們以金融核心鏈路里的賬務(wù)服務(wù)來分析一下哭当。首先一個最簡化的賬務(wù)模型就是圖中所列猪腕,每個用戶或商戶有一個賬戶及其可用余額。然后钦勘,分析下賬務(wù)服務(wù)的所有業(yè)務(wù)邏輯操作陋葡,無論是交易、充值彻采、轉(zhuǎn)賬脖岛、退款等,都可以認(rèn)為是對賬戶的加錢與扣錢颊亮。
因此柴梆,我們可以把賬務(wù)系統(tǒng)拆分成兩套 TCC 接口,即兩個 TCC Resource终惑,一個是加錢 TCC 接口绍在,一個是扣錢 TCC ?接口。
那這兩套接口分別需要做什么事情呢?如何將其分成兩個階段完成偿渡?下面將會舉例說明 TCC 業(yè)務(wù)模式的設(shè)計過程臼寄,并逐漸優(yōu)化。
我們先來看扣錢的 TCC 資源怎么實現(xiàn)溜宽。場景為 A 轉(zhuǎn)賬 30 元給 B吉拳。賬戶 A 的余額中有 100 元,需要扣除其中 30 元适揉。這里的余額就是所謂的業(yè)務(wù)資源留攒,按照前面提到的原則,在第一階段需要檢查并預(yù)留業(yè)務(wù)資源嫉嘀,因此炼邀,我們在扣錢 TCC 資源的 Try 接口里先檢查 A 賬戶余額是否足夠,然后預(yù)留余額里的業(yè)務(wù)資源剪侮,即扣除 30 元拭宁。
在 Confirm 接口,由于業(yè)務(wù)資源已經(jīng)在 Try 接口里扣除掉了瓣俯,那么在第二階段的 Confirm 接口里杰标,可以什么都不用做。而在 Cancel 接口里彩匕,則需要把 Try 接口里扣除掉的 30 元還給賬戶在旱。這是一個比較簡單的扣錢 TCC 資源的實現(xiàn),后面會繼續(xù)優(yōu)化它推掸。
而在加錢的 TCC 資源里桶蝎。在第一階段 Try 接口里不能直接給賬戶加錢,如果這個時候給賬戶增加了可用余額谅畅,那么在一階段執(zhí)行完后登渣,賬戶里的錢就可以被使用了。但是一階段執(zhí)行完以后毡泻,有可能是要回滾的胜茧。因此,真正加錢的動作需要放在 Confirm ?接口里仇味。對于加錢這個動作呻顽,第一階段 Try 接口里不需要預(yù)留任何資源,可以設(shè)計為空操作丹墨。那相應(yīng)的廊遍,Cancel 接口沒有資源需要釋放,也是一個空操作贩挣。只有真正需要提交時喉前,再在 Confirm 接口里給賬戶增加可用余額没酣。
這就是一個最簡單的扣錢和加錢的 TCC 資源的設(shè)計。在扣錢 TCC 資源里卵迂,Try 接口預(yù)留資源扣除余額裕便,Confirm 接口空操作,Cancel 接口釋放資源见咒,增加余額偿衰。在加錢 TCC 資源里,Try 接口無需預(yù)留資源改览,空操作下翎;Confirm 接口直接增加余額;Cancel 接口無需釋放資源恃疯,空操作。
2.3 賬務(wù)系統(tǒng)模型并發(fā)控制
之前提到墨闲,設(shè)計一套 TCC 接口需要有兩點今妄,一點是需要拆分業(yè)務(wù)邏輯成兩階段完成。這個我們已經(jīng)介紹了鸳碧。另外一點是要根據(jù)自身的業(yè)務(wù)模型控制并發(fā)盾鳞。
Seata 框架本身僅提供兩階段原子提交協(xié)議,保證分布式事務(wù)原子性瞻离。事務(wù)的隔離需要交給業(yè)務(wù)邏輯來實現(xiàn)腾仅。隔離的本質(zhì)就是控制并發(fā),防止并發(fā)事務(wù)操作相同資源而引起的結(jié)果錯亂套利。
舉個例子推励,比如金融行業(yè)里管理用戶資金,當(dāng)用戶發(fā)起交易時肉迫,一般會先檢查用戶資金验辞,如果資金充足,則扣除相應(yīng)交易金額喊衫,增加賣家資金跌造,完成交易。如果沒有事務(wù)隔離族购,用戶同時發(fā)起兩筆交易壳贪,兩筆交易的檢查都認(rèn)為資金充足,實際上卻只夠支付一筆交易寝杖,結(jié)果兩筆交易都支付成功违施,導(dǎo)致資損。
可以發(fā)現(xiàn)瑟幕,并發(fā)控制是業(yè)務(wù)邏輯執(zhí)行正確的保證醉拓,但是像兩階段鎖這樣的并發(fā)訪問控制技術(shù)要求一直持有數(shù)據(jù)庫資源鎖直到整個事務(wù)執(zhí)行結(jié)束伟姐,特別是在分布式事務(wù)架構(gòu)下,要求持有鎖到分布式事務(wù)第二階段執(zhí)行結(jié)束亿卤,也就是說愤兵,分布式事務(wù)會加長資源鎖的持有時間,導(dǎo)致并發(fā)性能進(jìn)一步下降排吴。
因此秆乳,TCC 模型的隔離性思想就是通過業(yè)務(wù)的改造,在第一階段結(jié)束之后钻哩,從底層數(shù)據(jù)庫資源層面的加鎖過渡為上層業(yè)務(wù)層面的加鎖屹堰,從而釋放底層數(shù)據(jù)庫鎖資源,放寬分布式事務(wù)鎖協(xié)議街氢,將鎖的粒度降到最低扯键,以最大限度提高業(yè)務(wù)并發(fā)性能。
還是以上面的例子舉例珊肃,“賬戶 A 上有 100 元荣刑,事務(wù) T1 要扣除其中的 30 元,事務(wù) T2 也要扣除 30 元伦乔,出現(xiàn)并發(fā)”厉亏。在第一階段 Try 操作中,需要先利用數(shù)據(jù)庫資源層面的加鎖烈和,檢查賬戶可用余額爱只,如果余額充足,則預(yù)留業(yè)務(wù)資源招刹,扣除本次交易金額恬试,一階段結(jié)束后,雖然數(shù)據(jù)庫層面資源鎖被釋放了疯暑,但這筆資金被業(yè)務(wù)隔離忘渔,不允許除本事務(wù)之外的其它并發(fā)事務(wù)動用。
并發(fā)的事務(wù) T2 在事務(wù) T1 一階段接口結(jié)束釋放了數(shù)據(jù)庫層面的資源鎖以后缰儿,就可以繼續(xù)操作畦粮,跟事務(wù) T1 一樣,加鎖乖阵,檢查余額宣赔,扣除交易金額。
事務(wù) T1 和 T2 分別扣除的那一部分資金瞪浸,相互之間無干擾儒将。這樣在分布式事務(wù)的二階段,無論 T1 是提交還是回滾对蒲,都不會對 T2 產(chǎn)生影響钩蚊,這樣 T1 和 T2 可以在同一個賬戶上并發(fā)執(zhí)行贡翘。
大家可以感受下,一階段結(jié)束以后砰逻,實際上采用業(yè)務(wù)加鎖的方式鸣驱,隔離賬戶資金,在第一階段結(jié)束后直接釋放底層資源鎖蝠咆,該用戶和賣家的其他交易都可以立刻并發(fā)執(zhí)行踊东,而不用等到整個分布式事務(wù)結(jié)束,可以獲得更高的并發(fā)交易能力刚操。
這里稍微有點抽象闸翅,下面我們將會針對業(yè)務(wù)模型進(jìn)行優(yōu)化,大家可以更直觀的感受業(yè)務(wù)加鎖的思想菊霜。
2.4 賬務(wù)系統(tǒng)模型優(yōu)化
前面的模型大家肯定會想坚冀,為啥一階段就把錢扣除了?是的鉴逞。之前只是為了簡單說明 TCC 模型的設(shè)計思想记某。在實際中,為了更好的用戶體驗华蜒,在第一階段辙纬,一般不會直接把賬戶的余額扣除豁遭,而是凍結(jié)叭喜,這樣給用戶展示的時候,就可以很清晰的知道蓖谢,哪些是可用余額捂蕴,哪些是凍結(jié)金額。
那業(yè)務(wù)模型變成什么樣了呢闪幽?如圖所示啥辨,需要在業(yè)務(wù)模型中增加凍結(jié)金額字段,用來表示賬戶有多少金額處以凍結(jié)狀態(tài)盯腌。
既然業(yè)務(wù)模型發(fā)生了變化溉知,那扣錢和加錢的 TCC 接口也應(yīng)該相應(yīng)的調(diào)整。還是以前面的例子來說明腕够。
在扣錢的 TCC 資源里级乍。Try 接口不再是直接扣除賬戶的可用余額,而是真正的預(yù)留資源帚湘,凍結(jié)部分可用余額玫荣,即減少可用余額,增加凍結(jié)金額大诸。Confirm 接口也不再是空操作捅厂,而是使用 Try 接口預(yù)留的業(yè)務(wù)資源贯卦,即將該部分凍結(jié)金額扣除;最后在 Cancel 接口里焙贷,就是釋放預(yù)留資源撵割,把 Try 接口的凍結(jié)金額扣除,增加賬戶可用余額盈厘。加錢的 TCC 資源由于不涉及凍結(jié)金額的使用永毅,所以無需更改。
通過這樣的優(yōu)化车酣,可以更直觀的感受到 TCC 接口的預(yù)留資源票灰、使用資源、釋放資源的過程契吉。
那并發(fā)控制又變成什么樣了呢跳仿?跟前面大部分類似,在事務(wù) T1 的第一階段 Try 操作中捐晶,先鎖定賬戶菲语,檢查賬戶可用余額,如果余額充足惑灵,則預(yù)留業(yè)務(wù)資源山上,減少可用余額,增加凍結(jié)金額英支。并發(fā)的事務(wù) T2 類似佩憾,加鎖,檢查余額干花,減少可用余額金額妄帘,增加凍結(jié)金額。
這里可以發(fā)現(xiàn)池凄,事務(wù) T1 和 T2 在一階段執(zhí)行完成后抡驼,都釋放了數(shù)據(jù)庫層面的資源鎖,但是在各自二階段的時候肿仑,相互之間并無干擾致盟,各自使用本事務(wù)內(nèi)第一階段Try接口內(nèi)凍結(jié)金額即可。這里大家就可以直觀感受到尤慰,在每個事務(wù)的第一階段馏锡,先通過數(shù)據(jù)庫層面的資源鎖,預(yù)留業(yè)務(wù)資源割择,即凍結(jié)金額眷篇。雖然在一階段結(jié)束以后,數(shù)據(jù)庫層面的資源鎖被釋放了荔泳,但是第二階段的執(zhí)行并不會被干擾蕉饼,這是因為數(shù)據(jù)庫層面資源鎖釋放以后通過業(yè)務(wù)隔離的方式為這部分資源加鎖虐杯,不允許除本事務(wù)之外的其它并發(fā)事務(wù)動用,從而保證該事務(wù)的第二階段能夠正確順利的執(zhí)行昧港。
通過這兩個例子擎椰,為大家講解了怎么去設(shè)計一套完備的 TCC 接口。最主要的有兩點创肥,一點是將業(yè)務(wù)邏輯拆分成兩個階段完成达舒,即 Try、Confirm叹侄、Cancel 接口巩搏。其中 ?Try 接口檢查資源、預(yù)留資源趾代、Confirm 使用資源贯底、Cancel 接口釋放預(yù)留資源。另外一點就是并發(fā)控制撒强,采用數(shù)據(jù)庫鎖與業(yè)務(wù)加鎖的方式結(jié)合禽捆。由于業(yè)務(wù)加鎖的特性不影響性能,因此飘哨,盡可能降低數(shù)據(jù)庫鎖粒度胚想,過渡為業(yè)務(wù)加鎖,從而提高業(yè)務(wù)并發(fā)能力芽隆。
3?TCC?異匙欠控制
在有了一套完備的 TCC 接口之后,是不是就真的高枕無憂了呢摆马?答案是否定的臼闻。在微服務(wù)架構(gòu)下鸿吆,很有可能出現(xiàn)網(wǎng)絡(luò)超時囤采、重發(fā),機器宕機等一系列的異常 Case惩淳。一旦遇到這些 Case蕉毯,就會導(dǎo)致我們的分布式事務(wù)執(zhí)行過程出現(xiàn)異常。根據(jù)螞蟻金服內(nèi)部多年的使用來看思犁,最常見的主要是這三種異常代虾,分別是空回滾、冪等激蹲、懸掛棉磨。
因此,TCC 接口里還需要解決這三類異常学辱。
實際上乘瓤,這三類問題可以在 Seata 框架里完成环形,只不過我們現(xiàn)在的 Seata 框架還不具備,之后我們會把這些異常 Case 的處理移植到 Seata 框架里衙傀,業(yè)務(wù)就無需關(guān)注這些異常情況抬吟,專注于業(yè)務(wù)邏輯即可。
雖然業(yè)務(wù)之后無需關(guān)心统抬,但是了解一下其內(nèi)部實現(xiàn)機制火本,也能更好的排查問題。
下面我將為大家一一講解這三類異常出現(xiàn)的原因以及對應(yīng)的解決方案聪建。
3.1 空回滾
首先是空回滾钙畔。什么是空回滾?空回滾就是對于一個分布式事務(wù)金麸,在沒有調(diào)用 TCC 資源 Try 方法的情況下刃鳄,調(diào)用了二階段的 Cancel 方法,Cancel 方法需要識別出這是一個空回滾钱骂,然后直接返回成功叔锐。
什么樣的情形會造成空回滾呢?
可以看圖中的第 2 步见秽,前面講過愉烙,注冊分支事務(wù)是在調(diào)用 RPC 時,Seata 框架的切面會攔截到該次調(diào)用請求解取,先向 TC 注冊一個分支事務(wù)步责,然后才去執(zhí)行 RPC 調(diào)用邏輯。如果 RPC 調(diào)用邏輯有問題禀苦,比如調(diào)用方機器宕機蔓肯、網(wǎng)絡(luò)異常,都會造成 RPC 調(diào)用失敗振乏,即未執(zhí)行 Try 方法蔗包。但是分布式事務(wù)已經(jīng)開啟了,需要推進(jìn)到終態(tài)慧邮,因此调限,TC 會回調(diào)參與者二階段 Cancel 接口,從而形成空回滾误澳。
那會不會有空提交呢耻矮?理論上來說不會的,如果調(diào)用方宕機忆谓,那分布式事務(wù)默認(rèn)是回滾的裆装。如果是網(wǎng)絡(luò)異常,那 RPC 調(diào)用失敗,發(fā)起方應(yīng)該通知 TC 回滾分布式事務(wù)哨免,這里可以看出為什么是理論上的勾扭,就是說發(fā)起方可以在 RPC 調(diào)用失敗的情況下依然通知 TC 提交,這時就會發(fā)生空提交铁瞒,這種情況要么是編碼問題妙色,要么開發(fā)同學(xué)明確知道需要這樣做。
那怎么解決空回滾呢慧耍?
前面提到身辨,Cancel 要識別出空回滾,直接返回成功芍碧。那關(guān)鍵就是要識別出這個空回滾煌珊。思路很簡單就是需要知道一階段是否執(zhí)行,如果執(zhí)行了泌豆,那就是正扯ㄢ郑回滾;如果沒執(zhí)行踪危,那就是空回滾蔬浙。因此,需要一張額外的事務(wù)控制表贞远,其中有分布式事務(wù) ID 和分支事務(wù) ID畴博,第一階段 Try 方法里會插入一條記錄,表示一階段執(zhí)行了蓝仲。Cancel 接口里讀取該記錄俱病,如果該記錄存在,則正掣そ幔回滾亮隙;如果該記錄不存在,則是空回滾垢夹。
3.2 冪等
接下來是冪等溢吻。冪等就是對于同一個分布式事務(wù)的同一個分支事務(wù),重復(fù)去調(diào)用該分支事務(wù)的第二階段接口棚饵,因此煤裙,要求 TCC 的二階段 Confirm 和 Cancel 接口保證冪等掩完,不會重復(fù)使用或者釋放資源噪漾。如果冪等控制沒有做好,很有可能導(dǎo)致資損等嚴(yán)重問題且蓬。
什么樣的情形會造成重復(fù)提交或回滾欣硼?從圖中可以看到,提交或回滾是一次 TC 到參與者的網(wǎng)絡(luò)調(diào)用恶阴。因此诈胜,網(wǎng)絡(luò)故障豹障、參與者宕機等都有可能造成參與者 TCC 資源實際執(zhí)行了二階段防范,但是 TC 沒有收到返回結(jié)果的情況焦匈,這時血公,TC 就會重復(fù)調(diào)用,直至調(diào)用成功缓熟,整個分布式事務(wù)結(jié)束累魔。
怎么解決重復(fù)執(zhí)行的冪等問題呢?一個簡單的思路就是記錄每個分支事務(wù)的執(zhí)行狀態(tài)够滑。在執(zhí)行前狀態(tài)垦写,如果已執(zhí)行,那就不再執(zhí)行彰触;否則梯投,正常執(zhí)行。前面在講空回滾的時候况毅,已經(jīng)有一張事務(wù)控制表了分蓖,事務(wù)控制表的每條記錄關(guān)聯(lián)一個分支事務(wù),那我們完全可以在這張事務(wù)控制表上加一個狀態(tài)字段尔许,用來記錄每個分支事務(wù)的執(zhí)行狀態(tài)咆疗。
如圖所示,該狀態(tài)字段有三個值母债,分別是初始化午磁、已提交、已回滾毡们。Try 方法插入時迅皇,是初始化狀態(tài)。二階段 Confirm 和 Cancel 方法執(zhí)行后修改為已提交或已回滾狀態(tài)衙熔。當(dāng)重復(fù)調(diào)用二階段接口時登颓,先獲取該事務(wù)控制表對應(yīng)記錄,檢查狀態(tài)红氯,如果已執(zhí)行框咙,則直接返回成功;否則正常執(zhí)行痢甘。
3.3 懸掛
最后是防懸掛喇嘱。按照慣例,咱們來先講講什么是懸掛塞栅。懸掛就是對于一個分布式事務(wù)者铜,其二階段 Cancel 接口比 Try 接口先執(zhí)行。因為允許空回滾的原因,Cancel 接口認(rèn)為 Try 接口沒執(zhí)行作烟,空回滾直接返回成功愉粤,對于 Seata 框架來說,認(rèn)為分布式事務(wù)的二階段接口已經(jīng)執(zhí)行成功拿撩,整個分布式事務(wù)就結(jié)束了衣厘。但是這之后 Try 方法才真正開始執(zhí)行,預(yù)留業(yè)務(wù)資源压恒,前面提到事務(wù)并發(fā)控制的業(yè)務(wù)加鎖头滔,對于一個 Try 方法預(yù)留的業(yè)務(wù)資源,只有該分布式事務(wù)才能使用涎显,然而 Seata 框架認(rèn)為該分布式事務(wù)已經(jīng)結(jié)束坤检,也就是說,當(dāng)出現(xiàn)這種情況時期吓,該分布式事務(wù)第一階段預(yù)留的業(yè)務(wù)資源就再也沒有人能夠處理了早歇,對于這種情況,我們就稱為懸掛讨勤,即業(yè)務(wù)資源預(yù)留后沒法繼續(xù)處理箭跳。
什么樣的情況會造成懸掛呢?
按照前面所講潭千,在 RPC 調(diào)用時谱姓,先注冊分支事務(wù),再執(zhí)行 RPC 調(diào)用刨晴,如果此時 RPC 調(diào)用的網(wǎng)絡(luò)發(fā)生擁堵屉来,通常 RPC 調(diào)用是有超時時間的,RPC 超時以后狈癞,發(fā)起方就會通知 TC 回滾該分布式事務(wù)茄靠,可能回滾完成后,RPC 請求才到達(dá)參與者蝶桶,真正執(zhí)行慨绳,從而造成懸掛。
怎么實現(xiàn)才能做到防懸掛呢真竖?根據(jù)懸掛出現(xiàn)的條件先來分析下脐雪,懸掛是指二階段 Cancel 執(zhí)行完后,一階段才執(zhí)行恢共。也就是說战秋,為了避免懸掛,如果二階段執(zhí)行完成旁振,那一階段就不能再繼續(xù)執(zhí)行获询。因此涨岁,當(dāng)一階段執(zhí)行時拐袜,需要先檢查二階段是否已經(jīng)執(zhí)行完成吉嚣,如果已經(jīng)執(zhí)行,則一階段不再執(zhí)行蹬铺;否則可以正常執(zhí)行尝哆。那怎么檢查二階段是否已經(jīng)執(zhí)行呢?大家是否想到了剛才解決空回滾和冪等時用到的事務(wù)控制表甜攀,可以在二階段執(zhí)行時插入一條事務(wù)控制記錄秋泄,狀態(tài)為已回滾,這樣當(dāng)一階段執(zhí)行時规阀,先讀取該記錄恒序,如果記錄存在,就認(rèn)為二階段已經(jīng)執(zhí)行谁撼;否則二階段沒執(zhí)行歧胁。
3.3 異常控制實現(xiàn)
在分析完空回滾厉碟、冪等喊巍、懸掛等異常 Case 的成因以及解決方案以后,下面我們就綜合起來考慮箍鼓,一個 TCC 接口如何完整的解決這三個問題崭参。
首先是 Try 方法。結(jié)合前面講到空回滾和懸掛異常款咖,Try 方法主要需要考慮兩個問題何暮,一個是 Try 方法需要能夠告訴二階段接口,已經(jīng)預(yù)留業(yè)務(wù)資源成功铐殃。
第二個是需要檢查第二階段是否已經(jīng)執(zhí)行完成郭卫,如果已完成,則不再執(zhí)行背稼。因此贰军,Try 方法的邏輯可以如圖所示:
先插入事務(wù)控制表記錄,如果插入成功蟹肘,說明第二階段還沒有執(zhí)行词疼,可以繼續(xù)執(zhí)行第一階段。如果插入失敗帘腹,則說明第二階段已經(jīng)執(zhí)行或正在執(zhí)行贰盗,則拋出異常,終止即可阳欲。
接下來是 Confirm 方法舵盈。因為 Confirm 方法不允許空回滾陋率,也就是說,Confirm 方法一定要在 Try 方法之后執(zhí)行秽晚。
因此瓦糟,Confirm 方法只需要關(guān)注重復(fù)提交的問題「坝可以先鎖定事務(wù)記錄菩浙,如果事務(wù)記錄為空,則說明是一個空提交句伶,不允許劲蜻,終止執(zhí)行。如果事務(wù)記錄不為空考余,則繼續(xù)檢查狀態(tài)是否為初始化先嬉,如果是,則說明一階段正確執(zhí)行楚堤,那二階段正常執(zhí)行即可疫蔓。如果狀態(tài)是已提交,則認(rèn)為是重復(fù)提交钾军,直接返回成功即可鳄袍;如果狀態(tài)是已回滾,也是一個異常吏恭,一個已回滾的事務(wù)拗小,不能重新提交,需要能夠攔截到這種異常情況樱哼,并報警哀九。
最后是 Cancel 方法。因為 Cancel 方法允許空回滾搅幅,并且要在先執(zhí)行的情況下阅束,讓 Try 方法感知到 Cancel 已經(jīng)執(zhí)行,所以和 Confirm 方法略有不同茄唐。
首先依然是鎖定事務(wù)記錄息裸。如果事務(wù)記錄為空,則認(rèn)為 Try 方法還沒執(zhí)行沪编,即是空回滾呼盆。空回滾的情況下蚁廓,應(yīng)該先插入一條事務(wù)記錄访圃,確保后續(xù)的 Try 方法不會再執(zhí)行。如果插入成功相嵌,則說明 Try 方法還沒有執(zhí)行腿时,空回滾繼續(xù)執(zhí)行况脆。如果插入失敗,則認(rèn)為Try 方法正再執(zhí)行批糟,等待 TC 的重試即可格了。如果一開始讀取事務(wù)記錄不為空,則說明 Try 方法已經(jīng)執(zhí)行完畢跃赚,再檢查狀態(tài)是否為初始化笆搓,如果是性湿,則還沒有執(zhí)行過其他二階段方法纬傲,正常執(zhí)行 Cancel 邏輯。如果狀態(tài)為已回滾肤频,則說明這是重復(fù)調(diào)用叹括,允許冪等,直接返回成功即可宵荒。如果狀態(tài)為已提交汁雷,則同樣是一個異常,一個已提交的事務(wù)报咳,不能再次回滾侠讯。
通過這一部分的講解,大家應(yīng)該對 TCC 模型下最常見的三類異常 Case暑刃,空回滾厢漩、冪等、懸掛的成因有所了解岩臣,也從實際例子中知道了怎么解決這三類異常溜嗜,在解決了這三類異常的情況下,我們的 TCC 接口設(shè)計就是比較完備的了架谎。
后續(xù)我們將會把這些解決方案移植到 Seata 框架中炸宵,由 Seata 框架來完成異常的處理,開發(fā) TCC 接口的同學(xué)就不再需要關(guān)心了谷扣。
4?TCC 性能優(yōu)化
雖然 TCC 模型已經(jīng)完備土全,但是隨著業(yè)務(wù)的增長,對于 TCC 模型的挑戰(zhàn)也越來越大会涎,可能還需要一些特殊的優(yōu)化裹匙,才能滿足業(yè)務(wù)需求。下面我們將會給大家講講在塔,螞蟻金服內(nèi)部在 TCC 模型上都做了哪些優(yōu)化幻件。
4.1 同庫模式
第一個優(yōu)化方案是改為同庫模式。同庫模式簡單來說蛔溃,就是分支事務(wù)記錄與業(yè)務(wù)數(shù)據(jù)在相同的庫中绰沥。什么意思呢篱蝇?之前提到,在注冊分支事務(wù)記錄的時候徽曲,框架的調(diào)用方切面會先向 TC 注冊一個分支事務(wù)記錄零截,注冊成功后,才會繼續(xù)往下執(zhí)行 RPC 調(diào)用秃臣。TC 在收到分支事務(wù)記錄注冊請求后涧衙,會往自己的數(shù)據(jù)庫里插入一條分支事務(wù)記錄,從而保證事務(wù)數(shù)據(jù)的持久化存儲奥此。那同庫模式就是調(diào)用方切面不再向 TC 注冊了弧哎,而是直接往業(yè)務(wù)的數(shù)據(jù)庫里插入一條事務(wù)記錄。
在講解同庫模式的性能優(yōu)化點之前稚虎,先給大家簡單講講同庫模式的恢復(fù)邏輯撤嫩。一個分布式事務(wù)的提交或回滾還是由發(fā)起方通知 TC,但是由于分支事務(wù)記錄保存在業(yè)務(wù)數(shù)據(jù)庫蠢终,而不是 TC 端序攘。因此,TC 不知道有哪些分支事務(wù)記錄寻拂,在收到提交或回滾的通知后程奠,僅僅是記錄一下該分布式事務(wù)的狀態(tài)。那分支事務(wù)記錄怎么真正執(zhí)行第二階段呢祭钉?需要在各個參與者內(nèi)部啟動一個異步任務(wù)瞄沙,定期撈取業(yè)務(wù)數(shù)據(jù)庫中未結(jié)束的分支事務(wù)記錄,然后向 TC 檢查整個分布式事務(wù)的狀態(tài)朴皆,即圖中的 StateCheckRequest 請求帕识。TC 在收到這個請求后,會根據(jù)之前保存的分布式事務(wù)的狀態(tài)遂铡,告訴參與者是提交還是回滾肮疗,從而完成分支事務(wù)記錄。
那這樣做有什么好處呢扒接?左邊是采用同庫模式前的調(diào)用關(guān)系圖伪货,在每次調(diào)用一個參與者的時候,都是先向 TC 注冊一個分布式事務(wù)記錄钾怔,TC 再持久化存儲在自己的數(shù)據(jù)庫中碱呼,也就是說,一個分支事務(wù)記錄的注冊宗侦,包含一次 RPC 和一次持久化存儲愚臀。
右邊是優(yōu)化后的調(diào)用關(guān)系圖。
從圖中可以看出矾利,每次調(diào)用一個參與者的時候姑裂,都是直接保存在業(yè)務(wù)的數(shù)據(jù)庫中馋袜,從而減少與 TC 之間的 RPC 調(diào)用。優(yōu)化后舶斧,有多少個參與者欣鳖,就節(jié)約多少次 RPC 調(diào)用。
這就是同庫模式的性能方案茴厉。
把分支事務(wù)記錄保存在業(yè)務(wù)數(shù)據(jù)庫中泽台,從而減少與 TC ?的 RPC 調(diào)用。
4.2 異步化
另外一個性能優(yōu)化方式就是異步化矾缓,什么是異步化怀酷。TCC 模型的一個作用就是把兩階段拆分成了兩個獨立的階段,通過資源業(yè)務(wù)鎖定的方式進(jìn)行關(guān)聯(lián)而账。資源業(yè)務(wù)鎖定方式的好處在于胰坟,既不會阻塞其他事務(wù)在第一階段對于相同資源的繼續(xù)使用因篇,也不會影響本事務(wù)第二階段的正確執(zhí)行泞辐。從理論上來說,只要業(yè)務(wù)允許竞滓,事務(wù)的第二階段什么時候執(zhí)行都可以咐吼,反正資源已經(jīng)業(yè)務(wù)鎖定,不會有其他事務(wù)動用該事務(wù)鎖定的資源商佑。
假設(shè)只有一個中間賬戶的情況下锯茄,每次調(diào)用支付服務(wù)的 Commit 接口,都會鎖定中間賬戶茶没,中間賬戶存在熱點性能問題肌幽。
但是,在擔(dān)保交易場景中抓半,七天以后才需要將資金從中間賬戶劃撥給商戶喂急,中間賬戶并不需要對外展示。因此笛求,在執(zhí)行完支付服務(wù)的第一階段后廊移,就可以認(rèn)為本次交易的支付環(huán)節(jié)已經(jīng)完成,并向用戶和商戶返回支付成功的結(jié)果探入,并不需要馬上執(zhí)行支付服務(wù)二階段的 Commit 接口狡孔,等到低鋒期時,再慢慢消化蜂嗽,異步地執(zhí)行苗膝。
5?總結(jié)
今天進(jìn)行了 Seata TCC 模式的深度解析。主要介紹 Seata TCC 模式的原理植旧,從 TCC 業(yè)務(wù)模型與并發(fā)控制的角度告訴大家怎么設(shè)計一個 TCC 的接口以及怎么處理空回滾辱揭、冪等芋类、懸掛等異常,最后對螞蟻金服內(nèi)部對 TCC 的性能優(yōu)化點簡單介紹界阁,使得 TCC 模式能夠滿足更高的業(yè)務(wù)需求侯繁。
業(yè)務(wù)各有不同,有些業(yè)務(wù)能容忍短期不一致泡躯,有些業(yè)務(wù)的操作可以冪等贮竟,無論什么樣的分布式事務(wù)解決方案都有其優(yōu)缺點,沒有一個銀彈能夠適配所有较剃。因此咕别,業(yè)務(wù)需要什么樣的解決方案,還需要結(jié)合自身的業(yè)務(wù)需求写穴、業(yè)務(wù)特點惰拱、技術(shù)架構(gòu)以及各解決方案的特性,綜合分析啊送,才能找到最適合的方案偿短。
如果大家對 Seata 的性能和需求有自己的想法,歡迎大家在釘釘群(搜索群號即可加入:23127468)或者 Github 上與我們討論交流馋没。
Seata:https://github.com/seata/seata