原文地址:梁桂釗的博客
歡迎關(guān)注公眾號:「服務(wù)端思維」。一群同頻者爽茴,一起成長葬凳,一起精進,打破認知的局限性室奏。
從本地事務(wù)到分布式事務(wù)的演變
什么是事務(wù)火焰?回答這個問題之前,我們先來看一個經(jīng)典的場景:支付寶等交易平臺的轉(zhuǎn)賬胧沫。假設(shè)小明需要用支付寶給小紅轉(zhuǎn)賬 100000 元昌简,此時,小明帳號會少 100000 元绒怨,而小紅帳號會多 100000 元纯赎。如果在轉(zhuǎn)賬過程中系統(tǒng)崩潰了,小明帳號少 100000 元南蹂,而小紅帳號金額不變犬金,就會出大問題,因此這個時候我們就需要使用事務(wù)了六剥。請參見圖 6-1晚顷。
這里,體現(xiàn)了事務(wù)一個很重要的特性:原子性疗疟。事實上该默,事務(wù)有四個基本特性:原子性、一致性策彤、隔離性权均、持久性顿膨。其中锅锨,原子性叽赊,即事務(wù)內(nèi)的操作要么全部成功,要么全部失敗必搞,不會在中間的某個環(huán)節(jié)結(jié)束必指。一致性,即使數(shù)據(jù)庫在一個事務(wù)執(zhí)行之前和執(zhí)行之后恕洲,數(shù)據(jù)庫都必須處于一致性狀態(tài)塔橡。如果事務(wù)執(zhí)行失敗,那么需要自動回滾到原始狀態(tài)霜第,換句話說葛家,事務(wù)一旦提交,其他事務(wù)查看到的結(jié)果一致泌类,事務(wù)一旦回滾癞谒,其他事務(wù)也只能看到回滾前的狀態(tài)。隔離性刃榨,即在并發(fā)環(huán)境中弹砚,不同的事務(wù)同時修改相同的數(shù)據(jù)時,一個未完成事務(wù)不會影響另外一個未完成事務(wù)枢希。持久性桌吃,即事務(wù)一旦提交,其修改的數(shù)據(jù)將永久保存到數(shù)據(jù)庫中苞轿,其改變是永久性的茅诱。
本地事務(wù)通過 ACID 保證數(shù)據(jù)的強一致性。ACID是 Atomic(原子性)搬卒、Consistency(一致性)瑟俭、 Isolation(隔離性)和 Durability(持久性)的縮寫 。在實際開發(fā)過程中秀睛,我們或多或少都有使用到本地事務(wù)尔当。例如,MySQL 事務(wù)處理使用到 begin 開始一個事務(wù)蹂安,rollback 事務(wù)回滾椭迎,commit 事務(wù)確認。這里田盈,事務(wù)提交后畜号,通過 redo log 記錄變更,通過 undo log 在失敗時進行回滾允瞧,保證事務(wù)的原子性简软。筆者補充下蛮拔,使用 Java 語言的開發(fā)者都接觸過 Spring。Spring 使用 @Transactional 注解就可以搞定事務(wù)功能痹升。事實上建炫,Spring 封裝了這些細節(jié),在生成相關(guān)的 Bean 的時候疼蛾,在需要注入相關(guān)的帶有 @Transactional 注解的 bean 時候用代理去注入肛跌,在代理中為我們開啟提交/回滾事務(wù)。請參見圖6-2察郁。
隨著業(yè)務(wù)的高速發(fā)展衍慎,面對海量數(shù)據(jù),例如皮钠,上千萬甚至上億的數(shù)據(jù)稳捆,查詢一次所花費的時間會變長,甚至會造成數(shù)據(jù)庫的單點壓力麦轰。因此乔夯,我們就要考慮分庫與分表方案了。分庫與分表的目的在于原朝,減小數(shù)據(jù)庫的單庫單表負擔驯嘱,提高查詢性能,縮短查詢時間喳坠。這里鞠评,我們先來看下單庫拆分的場景。事實上壕鹉,分表策略可以歸納為垂直拆分和水平拆分剃幌。垂直拆分,把表的字段進行拆分晾浴,即一張字段比較多的表拆分為多張表负乡,這樣使得行數(shù)據(jù)變小。一方面脊凰,可以減少客戶端程序和數(shù)據(jù)庫之間的網(wǎng)絡(luò)傳輸?shù)淖止?jié)數(shù)抖棘,因為生產(chǎn)環(huán)境共享同一個網(wǎng)絡(luò)帶寬,隨著并發(fā)查詢的增多狸涌,有可能造成帶寬瓶頸從而造成阻塞切省。另一方面,一個數(shù)據(jù)塊能存放更多的數(shù)據(jù)帕胆,在查詢時就會減少 I/O 次數(shù)朝捆。水平拆分,把表的行進行拆分懒豹。因為表的行數(shù)超過幾百萬行時芙盘,就會變慢驯用,這時可以把一張的表的數(shù)據(jù)拆成多張表來存放。水平拆分儒老,有許多策略蝴乔,例如,取模分表贷盲,時間維度分表等淘这。這種場景下,雖然我們根據(jù)特定規(guī)則分表了巩剖,我們?nèi)匀豢梢允褂帽镜厥聞?wù)。但是钠怯,庫內(nèi)分表佳魔,僅僅是解決了單表數(shù)據(jù)過大的問題,但并沒有把單表的數(shù)據(jù)分散到不同的物理機上晦炊,因此并不能減輕 MySQL 服務(wù)器的壓力鞠鲜,仍然存在同一個物理機上的資源競爭和瓶頸,包括 CPU断国、內(nèi)存贤姆、磁盤 IO、網(wǎng)絡(luò)帶寬等稳衬。對于分庫拆分的場景霞捡,它把一張表的數(shù)據(jù)劃分到不同的數(shù)據(jù)庫,多個數(shù)據(jù)庫的表結(jié)構(gòu)一樣薄疚。此時碧信,如果我們根據(jù)一定規(guī)則將我們需要使用事務(wù)的數(shù)據(jù)路由到相同的庫中,可以通過本地事務(wù)保證其強一致性街夭。但是砰碴,對于按照業(yè)務(wù)和功能劃分的垂直拆分,它將把業(yè)務(wù)數(shù)據(jù)分別放到不同的數(shù)據(jù)庫中板丽。這里呈枉,拆分后的系統(tǒng)就會遇到數(shù)據(jù)的一致性問題,因為我們需要通過事務(wù)保證的數(shù)據(jù)分散在不同的數(shù)據(jù)庫中埃碱,而每個數(shù)據(jù)庫只能保證自己的數(shù)據(jù)可以滿足 ACID 保證強一致性猖辫,但是在分布式系統(tǒng)中,它們可能部署在不同的服務(wù)器上乃正,只能通過網(wǎng)絡(luò)進行通信住册,因此無法準確的知道其他數(shù)據(jù)庫中的事務(wù)執(zhí)行情況。請參見圖6-3瓮具。
此外荧飞,不僅僅在跨庫調(diào)用存在本地事務(wù)無法解決的問題凡人,隨著微服務(wù)的落地中,每個服務(wù)都有自己的數(shù)據(jù)庫叹阔,并且數(shù)據(jù)庫是相互獨立且透明的挠轴。那如果服務(wù) A 需要獲取服務(wù) B 的數(shù)據(jù),就存在跨服務(wù)調(diào)用耳幢,如果遇到服務(wù)宕機岸晦,或者網(wǎng)絡(luò)連接異常、同步調(diào)用超時等場景就會導致數(shù)據(jù)的不一致睛藻,這個也是一種分布式場景下需要考慮數(shù)據(jù)一致性問題启上。請參見圖6-4。
總結(jié)一下店印,當業(yè)務(wù)量級擴大之后的分庫冈在,以及微服務(wù)落地之后的業(yè)務(wù)服務(wù)化,都會產(chǎn)生分布式數(shù)據(jù)不一致的問題按摘。既然本地事務(wù)無法滿足需求包券,因此分布式事務(wù)就要登上舞臺。什么是分布式事務(wù)炫贤?我們可以簡單地理解溅固,它就是為了保證不同數(shù)據(jù)庫的數(shù)據(jù)一致性的事務(wù)解決方案。這里兰珍,我們有必要先來了解下 CAP 原則和 BASE 理論侍郭。CAP 原則是 Consistency(一致性)、Availablity(可用性)和 Partition-tolerance(分區(qū)容錯性)的縮寫俩垃,它是分布式系統(tǒng)中的平衡理論励幼。在分布式系統(tǒng)中,一致性要求所有節(jié)點每次讀操作都能保證獲取到最新數(shù)據(jù)口柳;可用性要求無論任何故障產(chǎn)生后都能保證服務(wù)仍然可用苹粟;分區(qū)容錯性要求被分區(qū)的節(jié)點可以正常對外提供服務(wù)。事實上跃闹,任何系統(tǒng)只可同時滿足其中二個嵌削,無法三者兼顧。對于分布式系統(tǒng)而言望艺,分區(qū)容錯性是一個最基本的要求苛秕。那么,如果選擇了一致性和分區(qū)容錯性找默,放棄可用性艇劫,那么網(wǎng)絡(luò)問題會導致系統(tǒng)不可用。如果選擇可用性和分區(qū)容錯性惩激,放棄一致性店煞,不同的節(jié)點之間的數(shù)據(jù)不能及時同步數(shù)據(jù)而導致數(shù)據(jù)的不一致蟹演。請參見圖 6-5。
此時顷蟀,BASE 理論針對一致性和可用性提出了一個方案酒请,BASE 是 Basically Available(基本可用)、Soft-state(軟狀態(tài))和 Eventually Consistent(最終一致性)的縮寫鸣个,它是最終一致性的理論支撐羞反。簡單地理解,在分布式系統(tǒng)中囤萤,允許損失部分可用性昼窗,并且不同節(jié)點進行數(shù)據(jù)同步的過程存在延時,但是在經(jīng)過一段時間的修復后阁将,最終能夠達到數(shù)據(jù)的最終一致性膏秫。BASE 強調(diào)的是數(shù)據(jù)的最終一致性。相比于 ACID 而言做盅,BASE 通過允許損失部分一致性來獲得可用性。
現(xiàn)在窘哈,業(yè)內(nèi)比較常用的分布式事務(wù)解決方案吹榴,包括強一致性的兩階段提交協(xié)議,三階段提交協(xié)議滚婉,以及最終一致性的可靠事件模式图筹、補償模式,阿里的 TCC 模式让腹。我們會在后面的章節(jié)中詳細介紹與實戰(zhàn)远剩。
強一致性解決方案
二階段提交協(xié)議
在分布式系統(tǒng)中,每個數(shù)據(jù)庫只能保證自己的數(shù)據(jù)可以滿足 ACID 保證強一致性骇窍,但是它們可能部署在不同的服務(wù)器上瓜晤,只能通過網(wǎng)絡(luò)進行通信,因此無法準確的知道其他數(shù)據(jù)庫中的事務(wù)執(zhí)行情況腹纳。因此痢掠,為了解決多個節(jié)點之間的協(xié)調(diào)問題,就需要引入一個協(xié)調(diào)者負責控制所有節(jié)點的操作結(jié)果嘲恍,要么全部成功足画,要么全部失敗。其中佃牛,XA 協(xié)議是一個分布式事務(wù)協(xié)議淹辞,它有兩個角色:事務(wù)管理者和資源管理者。這里俘侠,我們可以把事務(wù)管理者理解為協(xié)調(diào)者象缀,而資源管理者理解為參與者及刻。
XA 協(xié)議通過二階段提交協(xié)議保證強一致性崇堰。
二階段提交協(xié)議,顧名思義,它具有兩個階段:第一階段準備与倡,第二階段提交。這里巡验,事務(wù)管理者(協(xié)調(diào)者)主要負責控制所有節(jié)點的操作結(jié)果损离,包括準備流程和提交流程。第一階段禁谦,事務(wù)管理者(協(xié)調(diào)者)向資源管理者(參與者)發(fā)起準備指令胁黑,詢問資源管理者(參與者)預(yù)提交是否成功。如果資源管理者(參與者)可以完成州泊,就會執(zhí)行操作丧蘸,并不提交,最后給出自己響應(yīng)結(jié)果遥皂,是預(yù)提交成功還是預(yù)提交失敗力喷。第二階段,如果全部資源管理者(參與者)都回復預(yù)提交成功演训,資源管理者(參與者)正式提交命令弟孟。如果其中有一個資源管理者(參與者)回復預(yù)提交失敗,則事務(wù)管理者(協(xié)調(diào)者)向所有的資源管理者(參與者)發(fā)起回滾命令样悟。舉個案例拂募,現(xiàn)在我們有一個事務(wù)管理者(協(xié)調(diào)者),三個資源管理者(參與者)窟她,那么這個事務(wù)中我們需要保證這三個參與者在事務(wù)過程中的數(shù)據(jù)的強一致性陈症。首先,事務(wù)管理者(協(xié)調(diào)者)發(fā)起準備指令預(yù)判它們是否已經(jīng)預(yù)提交成功了震糖,如果全部回復預(yù)提交成功录肯,那么事務(wù)管理者(協(xié)調(diào)者)正式發(fā)起提交命令執(zhí)行數(shù)據(jù)的變更。請參見圖 6-6试伙。
注意的是嘁信,雖然二階段提交協(xié)議為保證強一致性提出了一套解決方案,但是仍然存在一些問題疏叨。其一潘靖,事務(wù)管理者(協(xié)調(diào)者)主要負責控制所有節(jié)點的操作結(jié)果,包括準備流程和提交流程蚤蔓,但是整個流程是同步的卦溢,所以事務(wù)管理者(協(xié)調(diào)者)必須等待每一個資源管理者(參與者)返回操作結(jié)果后才能進行下一步操作。這樣就非常容易造成同步阻塞問題。其二单寂,單點故障也是需要認真考慮的問題贬芥。事務(wù)管理者(協(xié)調(diào)者)和資源管理者(參與者)都可能出現(xiàn)宕機,如果資源管理者(參與者)出現(xiàn)故障則無法響應(yīng)而一直等待宣决,事務(wù)管理者(協(xié)調(diào)者)出現(xiàn)故障則事務(wù)流程就失去了控制者蘸劈,換句話說,就是整個流程會一直阻塞尊沸,甚至極端的情況下威沫,一部分資源管理者(參與者)數(shù)據(jù)執(zhí)行提交,一部分沒有執(zhí)行提交洼专,也會出現(xiàn)數(shù)據(jù)不一致性棒掠。此時,讀者會提出疑問:這些問題應(yīng)該都是小概率情況屁商,一般是不會產(chǎn)生的烟很?是的,但是對于分布式事務(wù)場景蜡镶,我們不僅僅需要考慮正常邏輯流程雾袱,還需要關(guān)注小概率的異常場景,如果我們對異常場景缺乏處理方案官还,可能就會出現(xiàn)數(shù)據(jù)的不一致性谜酒,那么后期靠人工干預(yù)處理,會是一個成本非常大的任務(wù)妻枕,此外,對于交易的核心鏈路也許就不是數(shù)據(jù)問題粘驰,而是更加嚴重的資損問題屡谐。
三階段提交協(xié)議
二階段提交協(xié)議諸多問題,因此三階段提交協(xié)議就要登上舞臺了蝌数。三階段提交協(xié)議是二階段提交協(xié)議的改良版本愕掏,它與二階段提交協(xié)議不同之處在于,引入了超時機制解決同步阻塞問題顶伞,此外加入了預(yù)備階段盡可能提早發(fā)現(xiàn)無法執(zhí)行的資源管理者(參與者)并且終止事務(wù)饵撑,如果全部資源管理者(參與者)都可以完成,才發(fā)起第二階段的準備和第三階段的提交唆貌。否則滑潘,其中任何一個資源管理者(參與者)回復執(zhí)行,或者超時等待锨咙,那么就終止事務(wù)语卤。總結(jié)一下,三階段提交協(xié)議包括:第一階段預(yù)備粹舵,第二階段準備钮孵,第二階段提交。請參見圖 6-7眼滤。
三階段提交協(xié)議很好的解決了二階段提交協(xié)議帶來的問題巴席,是一個非常有參考意義的解決方案。但是诅需,極小概率的場景下可能會出現(xiàn)數(shù)據(jù)的不一致性漾唉。因為三階段提交協(xié)議引入了超時機制,如果出現(xiàn)資源管理者(參與者)超時場景會默認提交成功诱担,但是如果其沒有成功執(zhí)行毡证,或者其他資源管理者(參與者)出現(xiàn)回滾,那么就會出現(xiàn)數(shù)據(jù)的不一致性蔫仙。
最終一致性解決方案
TCC 模式
二階段提交協(xié)議和三階段提交協(xié)議很好的解決了分布式事務(wù)的問題料睛,但是在極端情況下仍然存在數(shù)據(jù)的不一致性,此外它對系統(tǒng)的開銷會比較大摇邦,引入事務(wù)管理者(協(xié)調(diào)者)后恤煞,比較容易出現(xiàn)單點瓶頸,以及在業(yè)務(wù)規(guī)模不斷變大的情況下施籍,系統(tǒng)可伸縮性也會存在問題居扒。注意的是,它是同步操作丑慎,因此引入事務(wù)后喜喂,直到全局事務(wù)結(jié)束才能釋放資源,性能可能是一個很大的問題竿裂。因此玉吁,在高并發(fā)場景下很少使用。因此腻异,阿里提出了另外一種解決方案:TCC 模式进副。注意的是,很多讀者把二階段提交等同于二階段提交協(xié)議悔常,這個是一個誤區(qū)影斑,事實上,TCC 模式也是一種二階段提交机打。
TCC 模式將一個任務(wù)拆分三個操作:Try矫户、Confirm、Cancel姐帚。假如吏垮,我們有一個 func() 方法障涯,那么在 TCC 模式中,它就變成了 tryFunc()膳汪、confirmFunc()唯蝶、cancelFunc() 三個方法。
tryFunc();
confirmFunc();
cancelFunc();
在 TCC 模式中遗嗽,主業(yè)務(wù)服務(wù)負責發(fā)起流程粘我,而從業(yè)務(wù)服務(wù)提供 TCC 模式的 Try、Confirm痹换、Cancel 三個操作征字。其中,還有一個事務(wù)管理器的角色負責控制事務(wù)的一致性娇豫。例如匙姜,我們現(xiàn)在有三個業(yè)務(wù)服務(wù):交易服務(wù),庫存服務(wù)冯痢,支付服務(wù)氮昧。用戶選商品,下訂單浦楣,緊接著選擇支付方式進行付款袖肥,然后這筆請求,交易服務(wù)會先調(diào)用庫存服務(wù)扣庫存振劳,然后交易服務(wù)再調(diào)用支付服務(wù)進行相關(guān)的支付操作椎组,然后支付服務(wù)會請求第三方支付平臺創(chuàng)建交易并扣款,這里历恐,交易服務(wù)就是主業(yè)務(wù)服務(wù)寸癌,而庫存服務(wù)和支付服務(wù)是從業(yè)務(wù)服務(wù)。請參見圖 6-8弱贼。
我們再來梳理下灵份,TCC 模式的流程。第一階段主業(yè)務(wù)服務(wù)調(diào)用全部的從業(yè)務(wù)服務(wù)的 Try 操作哮洽,并且事務(wù)管理器記錄操作日志。第二階段弦聂,當全部從業(yè)務(wù)服務(wù)都成功時鸟辅,再執(zhí)行 Confirm 操作,否則會執(zhí)行 Cancel 逆操作進行回滾莺葫。請參見圖 6-9匪凉。
現(xiàn)在,我們針對 TCC 模式說說大致業(yè)務(wù)上的實現(xiàn)思路捺檬。首先再层,交易服務(wù)(主業(yè)務(wù)服務(wù))會向事務(wù)管理器注冊并啟動事務(wù)。其實,事務(wù)管理器是一個概念上的全局事務(wù)管理機制聂受,可以是一個內(nèi)嵌于主業(yè)務(wù)服務(wù)的業(yè)務(wù)邏輯蒿秦,或者抽離出的一個 TCC 框架。事實上蛋济,它會生成全局事務(wù) ID 用于記錄整個事務(wù)鏈路棍鳖,并且實現(xiàn)了一套嵌套事務(wù)的處理邏輯。當主業(yè)務(wù)服務(wù)調(diào)用全部的從業(yè)務(wù)服務(wù)的 try 操作碗旅,事務(wù)管理器利用本地事務(wù)記錄相關(guān)事務(wù)日志渡处,這個案例中,它記錄了調(diào)用庫存服務(wù)的動作記錄祟辟,以及調(diào)用支付服務(wù)的動作記錄医瘫,并將其狀態(tài)設(shè)置成“預(yù)提交”狀態(tài)。這里旧困,調(diào)用從業(yè)務(wù)服務(wù)的 Try 操作就是核心的業(yè)務(wù)代碼醇份。那么, Try 操作怎么和它相對應(yīng)的 Confirm叮喳、Cancel 操作綁定呢被芳?其實,我們可以編寫配置文件建立綁定關(guān)系馍悟,或者通過 Spring 的注解添加 confirm 和 cancel 兩個參數(shù)也是不錯的選擇畔濒。當全部從業(yè)務(wù)服務(wù)都成功時,由事務(wù)管理器通過 TCC 事務(wù)上下文切面執(zhí)行 Confirm 操作锣咒,將其狀態(tài)設(shè)置成“成功”狀態(tài)侵状,否則執(zhí)行 Cancel 操作將其狀態(tài)設(shè)置成“預(yù)提交”狀態(tài),然后進行重試毅整。因此趣兄,TCC 模式通過補償?shù)姆绞奖WC其最終一致性。
TCC 的實現(xiàn)框架有很多成熟的開源項目悼嫉,例如 tcc-transaction 框架艇潭。(關(guān)于 tcc-transaction 框架的細節(jié),可以閱讀:https://github.com/changmingxie/tcc-transaction)tcc-transaction 框架主要涉及 tcc-transaction-core戏蔑、tcc-transaction-api蹋凝、tcc-transaction-spring 三個模塊。其中总棵,tcc-transaction-core 是 tcc-transaction 的底層實現(xiàn)鳍寂,tcc-transaction-api 是 tcc-transaction 使用的 API,tcc-transaction-spring 是 tcc-transaction 的 Spring 支持情龄。 tcc-transaction 將每個業(yè)務(wù)操作抽象成事務(wù)參與者迄汛,每個事務(wù)可以包含多個參與者捍壤。參與者需要聲明 try / confirm / cancel 三個類型的方法。這里鞍爱,我們通過 @Compensable 注解標記在 try 方法上鹃觉,并定義相應(yīng)的 confirm / cancel 方法。
// try 方法
@Compensable(confirmMethod = "confirmRecord", cancelMethod = "cancelRecord", transactionContextEditor = MethodTransactionContextEditor.class)
@Transactional
public String record(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) {}
// confirm 方法
@Transactional
public void confirmRecord(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) {}
// cancel 方法
@Transactional
public void cancelRecord(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) {}
對于 tcc-transaction 框架的實現(xiàn)硬霍,我們來了解一些核心思路帜慢。tcc-transaction 框架通過 @Compensable 切面進行攔截,可以透明化對參與者 confirm / cancel 方法調(diào)用唯卖,從而實現(xiàn) TCC 模式粱玲。這里,tcc-transaction 有兩個攔截器拜轨,請參見圖 6-10抽减。
org.mengyun.tcctransaction.interceptor.CompensableTransactionInterceptor,可補償事務(wù)攔截器橄碾。
org.mengyun.tcctransaction.interceptor.ResourceCoordinatorInterceptor卵沉,資源協(xié)調(diào)者攔截器。
這里法牲,需要特別關(guān)注 TransactionContext 事務(wù)上下文史汗,因為我們需要遠程調(diào)用服務(wù)的參與者時通過參數(shù)的形式傳遞事務(wù)給遠程參與者。在 tcc-transaction 中拒垃,一個事務(wù)org.mengyun.tcctransaction.Transaction
可以有多個參與者org.mengyun.tcctransaction.Participant
參與業(yè)務(wù)活動停撞。其中,事務(wù)編號 TransactionXid 用于唯一標識一個事務(wù)悼瓮,它使用 UUID 算法生成戈毒,保證唯一性。當參與者進行遠程調(diào)用時横堡,遠程的分支事務(wù)的事務(wù)編號等于該參與者的事務(wù)編號埋市。通過事務(wù)編號的關(guān)聯(lián) TCC confirm / cancel 方法,使用參與者的事務(wù)編號和遠程的分支事務(wù)進行關(guān)聯(lián)命贴,從而實現(xiàn)事務(wù)的提交和回滾道宅。事務(wù)狀態(tài) TransactionStatus 包含 : 嘗試中狀態(tài) TRYING(1)、確認中狀態(tài) CONFIRMING(2)胸蛛、取消中狀態(tài) CANCELLING(3)培己。此外,事務(wù)類型 TransactionType 包含 : 根事務(wù) ROOT(1)胚泌、分支事務(wù) BRANCH(2)。當調(diào)用 TransactionManager#begin() 發(fā)起根事務(wù)時肃弟,類型為 MethodType.ROOT玷室,并且事務(wù) try 方法被調(diào)用零蓉。調(diào)用 TransactionManager#propagationNewBegin() 方法,傳播發(fā)起分支事務(wù)穷缤。該方法在調(diào)用方法類型為 MethodType.PROVIDER 并且 事務(wù) try 方法被調(diào)用敌蜂。調(diào)用 TransactionManager#commit() 方法提交事務(wù)。該方法在事務(wù)處于 confirm / cancel 方法被調(diào)用津肛。類似地章喉,調(diào)用 TransactionManager#rollback() 方法,取消事務(wù)身坐。
此外秸脱,對于事務(wù)恢復機制,tcc-transaction 框架基于 Quartz 實現(xiàn)調(diào)度部蛇,按照一定頻率對事務(wù)進行重試摊唇,直到事務(wù)完成或超過最大重試次數(shù)。如果單個事務(wù)超過最大重試次數(shù)時涯鲁,tcc-transaction 框架不再重試巷查,此時需要手工介入解決。
這里抹腿,我們要特別注意操作的冪等性岛请。冪等機制的核心是保證資源唯一性,例如重復提交或服務(wù)端的多次重試只會產(chǎn)生一份結(jié)果警绩。支付場景崇败、退款場景,涉及金錢的交易不能出現(xiàn)多次扣款等問題房蝉。事實上僚匆,查詢接口用于獲取資源,因為它只是查詢數(shù)據(jù)而不會影響到資源的變化搭幻,因此不管調(diào)用多少次接口咧擂,資源都不會改變,所以是它是冪等的檀蹋。而新增接口是非冪等的松申,因為調(diào)用接口多次,它都將會產(chǎn)生資源的變化俯逾。因此贸桶,我們需要在出現(xiàn)重復提交時進行冪等處理。那么桌肴,如何保證冪等機制呢皇筛?事實上,我們有很多實現(xiàn)方案坠七。其中水醋,一種方案就是常見的創(chuàng)建唯一索引旗笔。在數(shù)據(jù)庫中針對我們需要約束的資源字段創(chuàng)建唯一索引,可以防止插入重復的數(shù)據(jù)拄踪。但是蝇恶,遇到分庫分表的情況是,唯一索引也就不那么好使了惶桐,此時撮弧,我們可以先查詢一次數(shù)據(jù)庫,然后判斷是否約束的資源字段存在重復姚糊,沒有的重復時再進行插入操作贿衍。注意的是,為了避免并發(fā)場景叛拷,我們可以通過鎖機制舌厨,例如悲觀鎖與樂觀鎖保證數(shù)據(jù)的唯一性。這里忿薇,分布式鎖是一種經(jīng)常使用的方案裙椭,它通常情況下是一種悲觀鎖的實現(xiàn)。但是署浩,很多人經(jīng)常把悲觀鎖揉燃、樂觀鎖、分布式鎖當作冪等機制的解決方案筋栋,這個是不正確的炊汤。除此之外,我們還可以引入狀態(tài)機弊攘,通過狀態(tài)機進行狀態(tài)的約束以及狀態(tài)跳轉(zhuǎn)抢腐,確保同一個業(yè)務(wù)的流程化執(zhí)行,從而實現(xiàn)數(shù)據(jù)冪等襟交。
補償模式
上節(jié)迈倍,我們提到了重試機制。事實上捣域,它也是一種最終一致性的解決方案:我們需要通過最大努力不斷重試啼染,保證數(shù)據(jù)庫的操作最終一定可以保證數(shù)據(jù)一致性,如果最終多次重試失敗可以根據(jù)相關(guān)日志并主動通知開發(fā)人員進行手工介入焕梅。注意的是迹鹅,被調(diào)用方需要保證其冪等性。重試機制可以是同步機制贞言,例如主業(yè)務(wù)服務(wù)調(diào)用超時或者非異常的調(diào)用失敗需要及時重新發(fā)起業(yè)務(wù)調(diào)用斜棚。重試機制可以大致分為固定次數(shù)的重試策略與固定時間的重試策略。除此之外,我們還可以借助消息隊列和定時任務(wù)機制弟蚀。消息隊列的重試機制脂新,即消息消費失敗則進行重新投遞,這樣就可以避免消息沒有被消費而被丟棄粗梭,例如 RocketMQ 可以默認允許每條消息最多重試 16 次,每次重試的間隔時間可以進行設(shè)置级零。定時任務(wù)的重試機制断医,我們可以創(chuàng)建一張任務(wù)執(zhí)行表,并增加一個“重試次數(shù)”字段奏纪。這種設(shè)計方案中鉴嗤,我們可以在定時調(diào)用時,獲取這個任務(wù)是否是執(zhí)行失敗的狀態(tài)并且沒有超過重試次數(shù)序调,如果是則進行失敗重試醉锅。但是,當出現(xiàn)執(zhí)行失敗的狀態(tài)并且超過重試次數(shù)時发绢,就說明這個任務(wù)永久失敗了硬耍,需要開發(fā)人員進行手工介入與排查問題。
除了重試機制之外边酒,也可以在每次更新的時候進行修復经柴。例如,對于社交互動的點贊數(shù)墩朦、收藏數(shù)坯认、評論數(shù)等計數(shù)場景,也許因為網(wǎng)絡(luò)抖動或者相關(guān)服務(wù)不可用氓涣,導致某段時間內(nèi)的數(shù)據(jù)不一致牛哺,我們就可以在每次更新的時候進行修復,保證系統(tǒng)經(jīng)過一段較短的時間的自我恢復和修正劳吠,數(shù)據(jù)最終達到一致引润。需要注意的是,使用這種解決方案的情況下赴背,如果某條數(shù)據(jù)出現(xiàn)不一致性椰拒,但是又沒有再次更新修復,那么其永遠都會是異常數(shù)據(jù)凰荚。
定時校對也是一種非常重要的解決手段燃观,它采取周期性的進行校驗操作來保證。關(guān)于定時任務(wù)框架的選型上便瑟,業(yè)內(nèi)比較常用的有單機場景下的 Quartz缆毁,以及分布式場景下 Elastic-Job、XXL-JOB到涂、SchedulerX 等分布式定時任務(wù)中間件脊框。關(guān)于定時校對可以分為兩種場景颁督,一種是未完成的定時重試,例如我們利用定時任務(wù)掃描還未完成的調(diào)用任務(wù)浇雹,并通過補償機制來修復沉御,實現(xiàn)數(shù)據(jù)最終達到一致。另一種是定時核對昭灵,它需要主業(yè)務(wù)服務(wù)提供相關(guān)查詢接口給從業(yè)務(wù)服務(wù)核對查詢吠裆,用于恢復丟失的業(yè)務(wù)數(shù)據(jù)。現(xiàn)在烂完,我們來試想一下電商場景的退款業(yè)務(wù)试疙。在這個退款業(yè)務(wù)中會存在一個退款基礎(chǔ)服務(wù)和自動化退款服務(wù)。此時抠蚣,自動化退款服務(wù)在退款基礎(chǔ)服務(wù)的基礎(chǔ)上實現(xiàn)退款能力的增強祝旷,實現(xiàn)基于多規(guī)則的自動化退款,并且通過消息隊列接收到退款基礎(chǔ)服務(wù)推送的退款快照信息嘶窄。但是怀跛,由于退款基礎(chǔ)服務(wù)發(fā)送消息丟失或者消息隊列在多次失敗重試后的主動丟棄,都很有可能造成數(shù)據(jù)的不一致性护侮。因此敌完,我們通過定時從退款基礎(chǔ)服務(wù)查詢核對,恢復丟失的業(yè)務(wù)數(shù)據(jù)就顯得特別重要了羊初。
可靠事件模式
在分布式系統(tǒng)中滨溉,消息隊列在服務(wù)端的架構(gòu)中的地位非常重要,主要解決異步處理长赞、系統(tǒng)解耦晦攒、流量削峰等場景。多個系統(tǒng)之間如果同步通信很容易造成阻塞得哆,同時會將這些系統(tǒng)會耦合在一起脯颜。因此,引入了消息隊列贩据,一方面解決了同步通信機制造成的阻塞栋操,另一方面通過消息隊列進行業(yè)務(wù)解耦。請參見圖 6-12饱亮。
可靠事件模式矾芙,通過引入可靠的消息隊列,只要保證當前的可靠事件投遞并且消息隊列確保事件傳遞至少一次近上,那么訂閱這個事件的消費者保證事件能夠在自己的業(yè)務(wù)內(nèi)被消費即可剔宪。這里,請讀者思考蘑险,是否只要引入了消息隊列就可以解決問題了呢芯急?事實上,只是引入消息隊列并不能保證其最終的一致性雹有,因為分布式部署環(huán)境下都是基于網(wǎng)絡(luò)進行通信地淀,而網(wǎng)絡(luò)通信過程中失球,上下游可能因為各種原因而導致消息丟失。
其一帮毁,主業(yè)務(wù)服務(wù)發(fā)送消息時可能因為消息隊列無法使用而發(fā)生失敗她倘。對于這種情況,我們可以讓主業(yè)務(wù)服務(wù)(生產(chǎn)者)發(fā)送消息作箍,再進行業(yè)務(wù)調(diào)用來確保。一般的做法是前硫,主業(yè)務(wù)服務(wù)將要發(fā)送的消息持久化到本地數(shù)據(jù)庫胞得,設(shè)置標志狀態(tài)為“待發(fā)送”狀態(tài),然后把消息發(fā)送給消息隊列屹电,消息隊列收到消息后阶剑,也把消息持久化到其存儲服務(wù)中,但并不是立即向從業(yè)務(wù)服務(wù)(消費者)投遞消息危号,而是先向主業(yè)務(wù)服務(wù)(生產(chǎn)者)返回消息隊列的響應(yīng)結(jié)果牧愁,然后主業(yè)務(wù)服務(wù)判斷響應(yīng)結(jié)果執(zhí)行之后的業(yè)務(wù)處理。如果響應(yīng)失敗外莲,則放棄之后的業(yè)務(wù)處理猪半,設(shè)置本地的持久化消息標志狀態(tài)為“結(jié)束”狀態(tài)。否則偷线,執(zhí)行后續(xù)的業(yè)務(wù)處理,設(shè)置本地的持久化消息標志狀態(tài)為“已發(fā)送”狀態(tài)声邦。
public void doServer(){
// 發(fā)送消息
send();
// 執(zhí)行業(yè)務(wù)
exec();
// 更新消息狀態(tài)
updateMsg();
}
此外,消息隊列發(fā)生消息后,也可能從業(yè)務(wù)服務(wù)(消費者)宕機而無法消費熊响。絕大多數(shù)消息中間件對于這種情況,例如 RabbitMQ叼屠、RocketMQ 等引入了 ACK 機制挑宠。注意的是,默認的情況下,采用自動應(yīng)答,這種方式中消息隊列會發(fā)送消息后立即從消息隊列中刪除該消息甸饱。所以沦童,為了確保消息的可靠投遞仑濒,我們通過手動 ACK 方式,如果從業(yè)務(wù)服務(wù)(消費者)因宕機等原因沒有發(fā)送 ACK偷遗,消息隊列會將消息重新發(fā)送墩瞳,保證消息的可靠性。從業(yè)務(wù)服務(wù)處理完相關(guān)業(yè)務(wù)后通過手動 ACK 通知消息隊列氏豌,消息隊列才從消息隊列中刪除該持久化消息喉酌。那么,消息隊列如果一直重試失敗而無法投遞泵喘,就會出現(xiàn)消息主動丟棄的情況泪电,我們需要如何解決呢?聰明的讀者可能已經(jīng)發(fā)現(xiàn)纪铺,我們在上個步驟中相速,主業(yè)務(wù)服務(wù)已經(jīng)將要發(fā)送的消息持久化到本地數(shù)據(jù)庫。因此鲜锚,從業(yè)務(wù)服務(wù)消費成功后和蚪,它也會向消息隊列發(fā)送一個通知消息,此時它是一個消息的生產(chǎn)者烹棉。主業(yè)務(wù)服務(wù)(消費者)接收到消息后,最終把本地的持久化消息標志狀態(tài)為“完成”狀態(tài)怯疤。說到這里浆洗,讀者應(yīng)該可以理解到我們使用“正反向消息機制”確保了消息隊列可靠事件投遞。當然集峦,補償機制也是必不可少的伏社。定時任務(wù)會從數(shù)據(jù)庫掃描在一定時間內(nèi)未完成的消息并重新投遞。請參見圖 6-13塔淤。
注意的是摘昌,因為從業(yè)務(wù)服務(wù)可能收到消息處理超時或者服務(wù)宕機,以及網(wǎng)絡(luò)等原因?qū)е露㈥犃惺詹坏较⒌奶幚斫Y(jié)果高蜂,因此可靠事件投遞并且消息隊列確保事件傳遞至少一次聪黎。這里,從業(yè)務(wù)服務(wù)(消費者)需要保證冪等性备恤。如果從業(yè)務(wù)服務(wù)(消費者)沒有保證接口的冪等性稿饰,將會導致重復提交等異常場景。此外露泊,我們也可以獨立消息服務(wù)喉镰,將消息服務(wù)獨立部署,根據(jù)不同的業(yè)務(wù)場景共用該消息服務(wù)惭笑,降低重復開發(fā)服務(wù)的成本侣姆。
了解了“可靠事件模式”的方法論后生真,現(xiàn)在我們來看一個真實的案例來加深理解。首先捺宗,當用戶發(fā)起退款后柱蟀,自動化退款服務(wù)會收到一個退款的事件消息,此時偿凭,如果這筆退款符合自動化退款策略的話产弹,自動化退款服務(wù)會先寫入本地數(shù)據(jù)庫持久化這筆退款快照,緊接著弯囊,發(fā)送一條執(zhí)行退款的消息投遞到給消息隊列痰哨,消息隊列接受到消息后返回響應(yīng)成功結(jié)果,那么自動化退款服務(wù)就可以執(zhí)行后續(xù)的業(yè)務(wù)邏輯匾嘱。與此同時斤斧,消息隊列異步地把消息投遞給退款基礎(chǔ)服務(wù),然后退款基礎(chǔ)服務(wù)執(zhí)行自己業(yè)務(wù)相關(guān)的邏輯霎烙,執(zhí)行失敗與否由退款基礎(chǔ)服務(wù)自我保證撬讽,如果執(zhí)行成功則發(fā)送一條執(zhí)行退款成功消息投遞到給消息隊列。最后悬垃,定時任務(wù)會從數(shù)據(jù)庫掃描在一定時間內(nèi)未完成的消息并重新投遞游昼。這里,需要注意的是尝蠕,自動化退款服務(wù)持久化的退款快照可以理解為需要確保投遞成功的消息烘豌,由“正反向消息機制”和“定時任務(wù)”確保其成功投遞。此外看彼,真正的退款出賬邏輯在退款基礎(chǔ)服務(wù)來保證廊佩,因此它要保證冪等性,及出賬邏輯的收斂靖榕。當出現(xiàn)執(zhí)行失敗的狀態(tài)并且超過重試次數(shù)時标锄,就說明這個任務(wù)永久失敗了,需要開發(fā)人員進行手工介入與排查問題茁计。請參見圖 6-14料皇。
總結(jié)一下,引入了消息隊列并不能保證可靠事件投遞星压,換句話說瓶蝴,由于網(wǎng)絡(luò)等各種原因而導致消息丟失不能保證其最終的一致性,因此租幕,我們需要通過“正反向消息機制”確保了消息隊列可靠事件投遞舷手,并且使用補償機制盡可能在一定時間內(nèi)未完成的消息并重新投遞。
開源項目的分布式事務(wù)實現(xiàn)解讀
開源項目中對分布式事務(wù)的應(yīng)用有很多值得我們學習與借鑒的地方劲绪。本節(jié)男窟,我們就來對其實現(xiàn)進行解讀盆赤。
RocketMQ
Apache RocketMQ 是阿里開源的一款高性能、高吞吐量的分布式消息中間件歉眷。在歷年雙 11 中牺六,RocketMQ 都承擔了阿里巴巴生產(chǎn)系統(tǒng)全部的消息流轉(zhuǎn),在核心交易鏈路有著穩(wěn)定和出色的表現(xiàn)汗捡,是承載交易峰值的核心基礎(chǔ)產(chǎn)品之一淑际。RocketMQ 同時存在商用版 MQ 可在阿里云上購買(https://www.aliyun.com/product/ons),阿里巴巴對于開源版本和商業(yè)版本扇住,主要區(qū)別在于:會開源分布式消息所有核心的特性春缕,而在商業(yè)層面,尤其是云平臺的搭建上面艘蹋,將運維管控锄贼、安全授權(quán)、深度培訓等納入商業(yè)重中之重女阀。
Apache RocketMQ 4.3 版本正式支持分布式事務(wù)消息宅荤。RocketMQ 事務(wù)消息設(shè)計主要解決了生產(chǎn)者端的消息發(fā)送與本地事務(wù)執(zhí)行的原子性問題,換句話說浸策,如果本地事務(wù)執(zhí)行不成功冯键,則不會進行 MQ 消息推送。那么庸汗,聰明的你可能就會存在疑問:我們可以先執(zhí)行本地事務(wù)惫确,執(zhí)行成功了再發(fā)送 MQ 消息,這樣不就可以保證事務(wù)性的夫晌?但是,請你再認真的思考下昧诱,如果 MQ 消息發(fā)送不成功怎么辦呢晓淀?事實上,RocketMQ 對此提供一個很好的思路和解決方案盏档。
RocketMQ 首先會發(fā)送預(yù)執(zhí)行消息到 MQ凶掰,并且在發(fā)送預(yù)執(zhí)行消息成功后執(zhí)行本地事務(wù)。緊接著蜈亩,它根據(jù)本地事務(wù)執(zhí)行結(jié)果進行后續(xù)執(zhí)行邏輯懦窘,如果本地事務(wù)執(zhí)行結(jié)果是 commit,那么正式投遞 MQ 消息稚配,如果本地事務(wù)執(zhí)行結(jié)果是 rollback畅涂,則 MQ 刪除之前投遞的預(yù)執(zhí)行消息,不進行投遞下發(fā)道川。注意的是午衰,對于異常情況立宜,例如執(zhí)行本地事務(wù)過程中,服務(wù)器宕機或者超時臊岸,RocketMQ 將會不停的詢問其同組的其他生產(chǎn)者端來獲取狀態(tài)橙数。請參見圖 6-15。
至此帅戒,我們已經(jīng)了解了 RocketMQ 的實現(xiàn)思路灯帮,如果對源碼實現(xiàn)感興趣的讀者,可以閱讀org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#sendMessageInTransaction
逻住。
ServiceComb
ServiceComb 基于華為內(nèi)部的 CSE(Cloud Service Engine) 框架開源而來钟哥,它提供了一套包含代碼框架生成,服務(wù)注冊發(fā)現(xiàn)鄙信,負載均衡瞪醋,服務(wù)可靠性(容錯熔斷,限流降級装诡,調(diào)用鏈追蹤)等功能的微服務(wù)框架银受。其中,ServiceComb Saga 是一個微服務(wù)應(yīng)用的數(shù)據(jù)最終一致性解決方案鸦采。
Saga 拆分分布式事務(wù)為多個本地事務(wù)宾巍,然后由 Saga 引擎負責協(xié)調(diào)。如果整個流程正常結(jié)束渔伯,那么業(yè)務(wù)成功完成顶霞;如果在這過程中實現(xiàn)出現(xiàn)部分失敗,那么Saga 引擎調(diào)用補償操作锣吼。Saga 有兩種恢復的策略 :向前恢復和向后恢復选浑。其中,向前恢復對失敗的節(jié)點采取最大努力不斷重試玄叠,保證數(shù)據(jù)庫的操作最終一定可以保證數(shù)據(jù)一致性古徒,如果最終多次重試失敗可以根據(jù)相關(guān)日志并主動通知開發(fā)人員進行手工介入。向后恢復對之前所有成功的節(jié)點執(zhí)行回滾的事務(wù)操作读恃,這樣保證數(shù)據(jù)達到一致的效果隧膘。
Saga 與 TCC 不同之處在于,Saga 比 TCC 少了一個 Try 操作寺惫。因此疹吃,Saga 會直接提交到數(shù)據(jù)庫,然后出現(xiàn)失敗的時候西雀,進行補償操作萨驶。Saga 的設(shè)計可能導致在極端場景下的補償動作比較麻煩,但是對于簡單的業(yè)務(wù)邏輯侵入性更低艇肴,更輕量級篡撵,并且減少了通信次數(shù)判莉,請參見圖 6-16。
ServiceComb Saga 在其理論基礎(chǔ)上進行了擴展育谬,它包含兩個組件: alpha 和 omega券盅。alpha 充當協(xié)調(diào)者,主要負責對事務(wù)的事件進行持久化存儲以及協(xié)調(diào)子事務(wù)的狀態(tài)膛檀,使其得以最終與全局事務(wù)的狀態(tài)保持一致锰镀。omega 是微服務(wù)中內(nèi)嵌的一個 agent,負責對網(wǎng)絡(luò)請求進行攔截并向 alpha 上報事務(wù)事件咖刃,并在異常情況下根據(jù) alpha 下發(fā)的指令執(zhí)行相應(yīng)的補償操作泳炉。在預(yù)處理階段,alpha 會記錄事務(wù)開始的事件嚎杨;在后處理階段花鹅,alpha 會記錄事務(wù)結(jié)束的事件。因此枫浙,每個成功的子事務(wù)都有一一對應(yīng)的開始及結(jié)束事件刨肃。在服務(wù)生產(chǎn)方,omega 會攔截請求中事務(wù)相關(guān)的 id 來提取事務(wù)的上下文箩帚。在服務(wù)消費方真友,omega 會在請求中注入事務(wù)相關(guān)的 id來傳遞事務(wù)的上下文。通過服務(wù)提供方和服務(wù)消費方的這種協(xié)作處理紧帕,子事務(wù)能連接起來形成一個完整的全局事務(wù)盔然。注意的是,Saga 要求相關(guān)的子事務(wù)提供事務(wù)處理方法是嗜,并且提供補償函數(shù)愈案。這里,添加 @EnableOmega 的注解來初始化 omega 的配置并與 alpha 建立連接鹅搪。在全局事務(wù)的起點添加 @SagaStart 的注解站绪,在子事務(wù)添加 @Compensable 的注解指明其對應(yīng)的補償方法。
使用案例:https://github.com/apache/servicecomb-saga/tree/master/saga-demo
@EnableOmega
public class Application{
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@SagaStart
public void xxx() { }
@Compensable
public void transfer() { }
現(xiàn)在涩嚣,我們來看一下它的業(yè)務(wù)流程圖崇众,請參見圖 6-17掂僵。
更多精彩文章航厚,盡在「服務(wù)端思維」!