前言
從 CPU 到內(nèi)存、到磁盤岛琼、到操作系統(tǒng)底循、到網(wǎng)絡,計算機系統(tǒng)處處存在不可靠因素槐瑞。工程師和科學家努力使用各種軟硬件方法對抗這種不可靠因素熙涤,保證數(shù)據(jù)和指令被正確地處理。在網(wǎng)絡領(lǐng)域有 TCP 可靠傳輸協(xié)議、在存儲領(lǐng)域有 Raid5 和 Raid6 算法灭袁、在數(shù)據(jù)庫領(lǐng)域有基于 ARIES 算法理論實現(xiàn)的事務機制……
這篇文章先介紹單機數(shù)據(jù)庫事務的 ACID 特性猬错,然后指出分布式場景下操作多數(shù)據(jù)源面臨的困境,引出分布式系統(tǒng)中常用的分布式事務解決方案茸歧,這些解決方案可以保證業(yè)務代碼在操作多個數(shù)據(jù)源的時候墨礁,能夠像操作單個數(shù)據(jù)源一樣履怯,具備 ACID 特性隘截。文章在最后給出業(yè)界較為成熟的分布式事務框架——Seata 的 AT 模式全局事務的實現(xiàn)掐松。
一鳖链、單數(shù)據(jù)源事務 & 多數(shù)據(jù)源事務
如果一個應用程序在一次業(yè)務流中通過連接驅(qū)動和數(shù)據(jù)源接口只連接并查詢(這里的查詢是廣義的竭望,包括增刪查改等)一個特定的數(shù)據(jù)庫宙拉,該應用程序就可以利用數(shù)據(jù)庫提供的事務機制(如果數(shù)據(jù)庫支持事務的話)保證對庫中記錄所進行的操作的可靠性例朱,這里的可靠性有四種語義:
原子性魔慷,A
一致性只锭,C
隔離性,I
持久性院尔,D
筆者在這里不再對這四種語義進行解釋蜻展,了解單數(shù)據(jù)源事務及其 ACID 特性是讀者閱讀這篇文章的前提。單個數(shù)據(jù)庫實現(xiàn)自身的事務特性是一個復雜又微妙的過程邀摆,例如 MySQL 的 InnoDB 引擎通過 Undo Log + Redo Log + ARIES 算法來實現(xiàn)纵顾。這是一個很宏大的話題,不在本文的描述范圍栋盹,讀者有興趣的話可自行研究施逾。
單數(shù)據(jù)源事務也可以叫做單機事務,或者本地事務例获。
在分布式場景下汉额,一個系統(tǒng)由多個子系統(tǒng)構(gòu)成,每個子系統(tǒng)有獨立的數(shù)據(jù)源榨汤。多個子系統(tǒng)之間通過互相調(diào)用來組合出更復雜的業(yè)務蠕搜。在時下流行的微服務系統(tǒng)架構(gòu)中,每一個子系統(tǒng)被稱作一個微服務件余,同樣每個微服務都維護自己的數(shù)據(jù)庫讥脐,以保持獨立性。
例如啼器,一個電商系統(tǒng)可能由購物微服務旬渠、庫存微服務、訂單微服務等組成端壳。購物微服務通過調(diào)用庫存微服務和訂單微服務來整合出購物業(yè)務告丢。用戶請求購物微服務商完成下單時,購物微服務一方面調(diào)用庫存微服務扣減相應商品的庫存數(shù)量损谦,另一方面調(diào)用訂單微服務插入訂單記錄(為了后文描述分布式事務解決方案的方便岖免,這里給出的是一個最簡單的電商系統(tǒng)微服務劃分和最簡單的購物業(yè)務流程岳颇,后續(xù)的支付、物流等業(yè)務不在考慮范圍內(nèi))颅湘。電商系統(tǒng)模型如下圖所示:
在用戶購物的業(yè)務場景中话侧,shopping-service 的業(yè)務涉及兩個數(shù)據(jù)庫:庫存庫(repo_db)和訂單庫(repo_db),也就是 g 購物業(yè)務是調(diào)用多數(shù)據(jù)源來組合而成的闯参。作為一個面向消費者的系統(tǒng)瞻鹏,電商系統(tǒng)要保證購物業(yè)務的高度可靠性,這里的可靠性同樣有 ACID 四種語義鹿寨。
但是一個數(shù)據(jù)庫的本地事務機制僅僅對落到自己身上的查詢操作(這里的查詢是廣義的新博,包括增刪改查等)起作用,無法干涉對其他數(shù)據(jù)庫的查詢操作脚草。所以赫悄,數(shù)據(jù)庫自身提供的本地事務機制無法確保業(yè)務對多數(shù)據(jù)源全局操作的可靠性。
基于此馏慨,針對多數(shù)據(jù)源操作提出的分布式事務機制就出現(xiàn)了埂淮。
分布式事務也可以叫做全局事務。
二熏纯、常見分布式事務解決方案
2.1 分布式事務模型
描述分布式事務同诫,常常會使用以下幾個名詞:
事務參與者:例如每個數(shù)據(jù)庫就是一個事務參與者
事務協(xié)調(diào)者:訪問多個數(shù)據(jù)源的服務程序,例如 shopping-service 就是事務協(xié)調(diào)者
資源管理器(Resource Manager, RM):通常與事務參與者同義
事務管理器(Transaction Manager, TM):通常與事務協(xié)調(diào)者同義
在分布式事務模型中樟澜,一個 TM 管理多個 RM误窖,即一個服務程序訪問多個數(shù)據(jù)源;TM 是一個全局事務管理器秩贰,協(xié)調(diào)多方本地事務的進度霹俺,使其共同提交或回滾,最終達成一種全局的 ACID 特性毒费。
2.2 二將軍問題和冪等性
二將軍問題是網(wǎng)絡領(lǐng)域的一個經(jīng)典問題丙唧,用于表達計算機網(wǎng)絡中互聯(lián)協(xié)議設計的微妙性和復雜性。這里給出一個二將軍問題的簡化版本:
一支白軍被圍困在一個山谷中觅玻,山谷的左右兩側(cè)是藍軍想际。困在山谷中的白軍人數(shù)多于山谷兩側(cè)的任意一支藍軍,而少于兩支藍軍的之和溪厘。若一支藍軍對白軍單獨發(fā)起進攻胡本,則必敗無疑;但若兩支藍軍同時發(fā)起進攻畸悬,則可取勝侧甫。兩只藍軍的總指揮位于山谷左側(cè),他希望兩支藍軍同時發(fā)起進攻,這樣就要把命令傳到山谷右側(cè)的藍軍披粟,以告知發(fā)起進攻的具體時間咒锻。假設他們只能派遣士兵穿越白軍所在的山谷(唯一的通信信道)來傳遞消息,那么在穿越山谷時守屉,士兵有可能被俘虜惑艇。
只有當送信士兵成功往返后,總指揮才能確認這場戰(zhàn)爭的勝利(上方圖)⌒匕穑現(xiàn)在問題來了敦捧,派遣出去送信的士兵沒有回來,則左側(cè)藍軍中的總指揮能不能決定按命令中約定的時間發(fā)起進攻碰镜?
答案是不確定,派遣出去送信的士兵沒有回來习瑰,他可能遇到兩種狀況:
1)命令還沒送達就被俘虜了(中間圖)绪颖,這時候右側(cè)藍軍根本不知道要何時進攻;
2)命令送達甜奄,但返回途中被俘虜了(下方圖)柠横,這時候右側(cè)藍軍知道要何時進攻,但左側(cè)藍軍不知道右側(cè)藍軍是否知曉進攻時間课兄。
類似的問題在計算機網(wǎng)絡中普遍存在牍氛,例如發(fā)送者給接受者發(fā)送一個 HTTP 請求,或者 MySQL 客戶端向 MySQL 服務器發(fā)送一條插入語句烟阐,然后超時了沒有得到響應搬俊。請問服務器是寫入成功了還是失敗了?答案是不確定蜒茄,有以下幾種情況:
1)可能請求由于網(wǎng)絡故障根本沒有送到服務器唉擂,因此寫入失敗檀葛;
2)可能服務器收到了玩祟,也寫入成功了,但是向客戶端發(fā)送響應前服務器宕機了屿聋;
3)可能服務器收到了空扎,也寫入成功了,也向客戶端發(fā)送了響應润讥,但是由于網(wǎng)絡故障未送到客戶端转锈。
無論哪種場景,在客戶端看來都是一樣的結(jié)果:它發(fā)出的請求沒有得到響應象对。為了確保服務端成功寫入數(shù)據(jù)黑忱,客戶端只能重發(fā)請求,直至接收到服務端的響應。
類似的問題問題被稱為網(wǎng)絡二將軍問題甫煞。
網(wǎng)絡二將軍問題的存在使得消息的發(fā)送者往往要重復發(fā)送消息菇曲,直到收到接收者的確認才認為發(fā)送成功,但這往往又會導致消息的重復發(fā)送抚吠。例如電商系統(tǒng)中訂單模塊調(diào)用支付模塊扣款的時候常潮,如果網(wǎng)絡故障導致二將軍問題出現(xiàn),扣款請求重復發(fā)送楷力,產(chǎn)生的重復扣款結(jié)果顯然是不能被接受的喊式。因此要保證一次事務中的扣款請求無論被發(fā)送多少次,接收方有且只執(zhí)行一次扣款動作萧朝,這種保證機制叫做接收方的冪等性岔留。
2.3 兩階段提交(2PC) & 三階段提交(3PC)方案
2PC 是一種實現(xiàn)分布式事務的簡單模型,這兩個階段是:
1)準備階段:事務協(xié)調(diào)者向各個事務參與者發(fā)起詢問請求:“我要執(zhí)行全局事務了检柬,這個事務涉及到的資源分布在你們這些數(shù)據(jù)源中献联,分別是……,你們準備好各自的資源(即各自執(zhí)行本地事務到待提交階段)”何址。各個參與者協(xié)調(diào)者回復 yes(表示已準備好里逆,允許提交全局事務)或 no(表示本參與者無法拿到全局事務所需的本地資源,因為它被其他本地事務鎖住了)或超時用爪。
2)提交階段:如果各個參與者回復的都是 yes原押,則協(xié)調(diào)者向所有參與者發(fā)起事務提交操作,然后所有參與者收到后各自執(zhí)行本地事務提交操作并向協(xié)調(diào)者發(fā)送 ACK偎血;如果任何一個參與者回復 no 或者超時诸衔,則協(xié)調(diào)者向所有參與者發(fā)起事務回滾操作,然后所有參與者收到后各自執(zhí)行本地事務回滾操作并向協(xié)調(diào)者發(fā)送 ACK烁巫。
2PC 的流程如下圖所示:
從上圖可以看出署隘,要實現(xiàn) 2PC,所有的參與者都要實現(xiàn)三個接口:
Prepare():TM 調(diào)用該接口詢問各個本地事務是否就緒
Commit():TM 調(diào)用該接口要求各個本地事務提交
Rollback():TM 調(diào)用該接口要求各個本地事務回滾
可以將這三個接口簡單地(但不嚴謹?shù)兀├斫獬?XA 協(xié)議亚隙。XA 協(xié)議是 X/Open 提出的分布式事務處理標準磁餐。MySQL、Oracle阿弃、DB2 這些主流數(shù)據(jù)庫都實現(xiàn)了 XA 協(xié)議诊霹,因此都能被用于實現(xiàn) 2PC 事務模型。
2PC 簡明易懂渣淳,但存在如下的問題:
1)性能差脾还,在準備階段,要等待所有的參與者返回入愧,才能進入階段二鄙漏,在這期間嗤谚,各個參與者上面的相關(guān)資源被排他地鎖住,參與者上面意圖使用這些資源的本地事務只能等待怔蚌。因為存在這種同步阻塞問題巩步,所以影響了各個參與者的本地事務并發(fā)度;
2)準備階段完成后桦踊,如果協(xié)調(diào)者宕機椅野,所有的參與者都收不到提交或回滾指令,導致所有參與者“不知所措”籍胯;
3)在提交階段竟闪,協(xié)調(diào)者向所有的參與者發(fā)送了提交指令,如果一個參與者未返回 ACK杖狼,那么協(xié)調(diào)者不知道這個參與者內(nèi)部發(fā)生了什么(由于網(wǎng)絡二將軍問題的存在炼蛤,這個參與者可能根本沒收到提交指令,一直處于等待接收提交指令的狀態(tài)蝶涩;也可能收到了鲸湃,并成功執(zhí)行了本地提交,但返回的 ACK 由于網(wǎng)絡故障未送到協(xié)調(diào)者上)子寓,也就無法決定下一步是否進行全體參與者的回滾。
2PC 之后又出現(xiàn)了 3PC笋除,把兩階段過程變成了三階段過程斜友,分別是:詢問階段、準備階段垃它、提交或回滾階段鲜屏,這里不再詳述。3PC 利用超時機制解決了 2PC 的同步阻塞問題国拇,避免資源被永久鎖定洛史,進一步加強了整個事務過程的可靠性。但是 3PC 同樣無法應對類似的宕機問題酱吝,只不過出現(xiàn)多數(shù)據(jù)源中數(shù)據(jù)不一致問題的概率更小也殖。
2PC 除了性能和可靠性上存在問題,它的適用場景也很局限务热,它要求參與者實現(xiàn)了 XA 協(xié)議忆嗜,例如使用實現(xiàn)了 XA 協(xié)議的數(shù)據(jù)庫作為參與者可以完成 2PC 過程。但是在多個系統(tǒng)服務利用 api 接口相互調(diào)用的時候崎岂,就不遵守 XA 協(xié)議了捆毫,這時候 2PC 就不適用了。所以 2PC 在分布式應用場景中很少使用冲甘。
所以前文提到的電商場景無法使用 2PC绩卤,因為 shopping-service 通過 RPC 接口或者 Rest 接口調(diào)用 repo-service 和 order-service 間接訪問 repo_db 和 order_db途样。除非 shopping-service 直接配置 repo_db 和 order_db 作為自己的數(shù)據(jù)庫。
2.4 TCC 方案
描述 TCC 方案使用的電商微服務模型如下圖所示濒憋,在這個模型中何暇,shopping-service 是事務協(xié)調(diào)者,repo-service 和 order-service 是事務參與者跋炕。
上文提到赖晶,2PC 要求參與者實現(xiàn)了 XA 協(xié)議,通常用來解決多個數(shù)據(jù)庫之間的事務問題辐烂,比較局限遏插。在多個系統(tǒng)服務利用 api 接口相互調(diào)用的時候,就不遵守 XA 協(xié)議了纠修,這時候 2PC 就不適用了「斐埃現(xiàn)代企業(yè)多采用分布式的微服務,因此更多的是要解決多個微服務之間的分布式事務問題扣草。
TCC 就是一種解決多個微服務之間的分布式事務問題的方案了牛。TCC 是 Try、Confirm辰妙、Cancel 三個詞的縮寫鹰祸,其本質(zhì)是一個應用層面上的 2PC,同樣分為兩個階段:
1)階段一:準備階段密浑。協(xié)調(diào)者調(diào)用所有的每個微服務提供的 try 接口蛙婴,將整個全局事務涉及到的資源鎖定住,若鎖定成功 try 接口向協(xié)調(diào)者返回 yes尔破。
2)階段二:提交階段街图。若所有的服務的 try 接口在階段一都返回 yes,則進入提交階段懒构,協(xié)調(diào)者調(diào)用所有服務的 confirm 接口餐济,各個服務進行事務提交。如果有任何一個服務的 try 接口在階段一返回 no 或者超時胆剧,則協(xié)調(diào)者調(diào)用所有服務的 cancel 接口絮姆。
TCC 的流程如下圖所示:
這里有個關(guān)鍵問題,既然 TCC 是一種服務層面上的 2PC赞赖,它是如何解決 2PC 無法應對宕機問題的缺陷的呢滚朵?答案是不斷重試。由于 try 操作鎖住了全局事務涉及的所有資源前域,保證了業(yè)務操作的所有前置條件得到滿足辕近,因此無論是 confirm 階段失敗還是 cancel 階段失敗都能通過不斷重試直至 confirm 或 cancel 成功(所謂成功就是所有的服務都對 confirm 或者 cancel 返回了 ACK)。
這里還有個關(guān)鍵問題匿垄,在不斷重試 confirm 和 cancel 的過程中(考慮到網(wǎng)絡二將軍問題的存在)有可能重復進行了 confirm 或 cancel移宅,因此還要再保證 confirm 和 cancel 操作具有冪等性归粉,也就是整個全局事務中,每個參與者只進行一次 confirm 或者 cancel漏峰。實現(xiàn) confirm 和 cancel 操作的冪等性糠悼,有很多解決方案,例如每個參與者可以維護一個去重表(可以利用數(shù)據(jù)庫表實現(xiàn)也可以使用內(nèi)存型 KV 組件實現(xiàn))浅乔,記錄每個全局事務(以全局事務標記 XID 區(qū)分)是否進行過 confirm 或 cancel 操作倔喂,若已經(jīng)進行過,則不再重復執(zhí)行靖苇。
TCC 由支付寶團隊提出席噩,被廣泛應用于金融系統(tǒng)中。我們用銀行賬戶余額購買基金時贤壁,會注意到銀行賬戶中用于購買基金的那部分余額首先會被凍結(jié)悼枢,由此我們可以猜想,這個過程大概就是 TCC 的第一階段脾拆。
2.5 事務狀態(tài)表方案
另外有一種類似 TCC 的事務解決方案馒索,借助事務狀態(tài)表來實現(xiàn)。假設要在一個分布式事務中實現(xiàn)調(diào)用 repo-service 扣減庫存名船、調(diào)用 order-service 生成訂單兩個過程绰上。在這種方案中,協(xié)調(diào)者 shopping-service 維護一張如下的事務狀態(tài)表:
初始狀態(tài)為 1渠驼,每成功調(diào)用一個服務則更新一次狀態(tài)渔期,最后所有的服務調(diào)用成功,狀態(tài)更新到 3渴邦。
有了這張表,就可以啟動一個后臺任務拘哨,掃描這張表中事務的狀態(tài)谋梭,如果一個分布式事務一直(設置一個事務周期閾值)未到狀態(tài) 3,說明這條事務沒有成功執(zhí)行倦青,于是可以重新調(diào)用 repo-service 扣減庫存瓮床、調(diào)用 order-service 生成訂單。直至所有的調(diào)用成功产镐,事務狀態(tài)到 3隘庄。
如果多次重試仍未使得狀態(tài)到 3,可以將事務狀態(tài)置為 error癣亚,通過人工介入進行干預丑掺。
由于存在服務的調(diào)用重試,因此每個服務的接口要根據(jù)全局的分布式事務 ID 做冪等述雾,原理同 2.4 節(jié)的冪等性實現(xiàn)街州。
2.7 基于消息中間件的最終一致性事務方案
無論是 2PC & 3PC 還是 TCC兼丰、事務狀態(tài)表,基本都遵守 XA 協(xié)議的思想唆缴,即這些方案本質(zhì)上都是事務協(xié)調(diào)者協(xié)調(diào)各個事務參與者的本地事務的進度鳍征,使所有本地事務共同提交或回滾,最終達成一種全局的 ACID 特性面徽。在協(xié)調(diào)的過程中艳丛,協(xié)調(diào)者需要收集各個本地事務的當前狀態(tài),并根據(jù)這些狀態(tài)發(fā)出下一階段的操作指令趟紊。
但是這些全局事務方案由于操作繁瑣氮双、時間跨度大,或者在全局事務期間會排他地鎖住相關(guān)資源织阳,使得整個分布式系統(tǒng)的全局事務的并發(fā)度不會太高眶蕉。這很難滿足電商等高并發(fā)場景對事務吞吐量的要求,因此互聯(lián)網(wǎng)服務提供商探索出了很多與 XA 協(xié)議背道而馳的分布式事務解決方案唧躲。其中利用消息中間件實現(xiàn)的最終一致性全局事務就是一個經(jīng)典方案造挽。
為了表現(xiàn)出這種方案的精髓,我將使用如下的電商系統(tǒng)微服務結(jié)構(gòu)來進行描述:
在這個模型中弄痹,用戶不再是請求整合后的 shopping-service 進行下單饭入,而是直接請求 order-service 下單,order-service 一方面添加訂單記錄肛真,另一方面會調(diào)用 repo-service 扣減庫存谐丢。
這種基于消息中間件的最終一致性事務方案常常被誤解成如下的實現(xiàn)方式:
這種實現(xiàn)方式的流程是:
1)order-service 負責向 MQ server 發(fā)送扣減庫存消息(repo_deduction_msg);repo-service 訂閱 MQ server 中的扣減庫存消息蚓让,負責消費消息债蓝。
2)用戶下單后,order-service 先執(zhí)行插入訂單記錄的查詢語句毯侦,后將 repo_deduction_msg 發(fā)到消息中間件中米死,這兩個過程放在一個本地事務中進行,一旦“執(zhí)行插入訂單記錄的查詢語句”失敗趟卸,導致事務回滾蹄葱,“將 repo_deduction_msg 發(fā)到消息中間件中”就不會發(fā)生;同樣锄列,一旦“將 repo_deduction_msg 發(fā)到消息中間件中”失敗图云,拋出異常,也會導致“執(zhí)行插入訂單記錄的查詢語句”操作回滾邻邮,最終什么也沒有發(fā)生竣况。
3)repo-service 接收到 repo_deduction_msg 之后,先執(zhí)行庫存扣減查詢語句筒严,后向 MQ sever 反饋消息消費完成 ACK帕翻,這兩個過程放在一個本地事務中進行鸠补,一旦“執(zhí)行庫存扣減查詢語句”失敗,導致事務回滾嘀掸,“向 MQ sever 反饋消息消費完成 ACK”就不會發(fā)生紫岩,MQ server 在 Confirm 機制的驅(qū)動下會繼續(xù)向 repo-service 推送該消息,直到整個事務成功提交睬塌;同樣泉蝌,一旦“向 MQ sever 反饋消息消費完成 ACK”失敗,拋出異常揩晴,也對導致“執(zhí)行庫存扣減查詢語句”操作回滾勋陪,MQ server 在 Confirm 機制的驅(qū)動下會繼續(xù)向 repo-service 推送該消息,直到整個事務成功提交硫兰。
這種做法看似很可靠诅愚。但沒有考慮到網(wǎng)絡二將軍問題的存在,有如下的缺陷:
1)存在網(wǎng)絡的 2 將軍問題劫映,上面第 2)步中 order-service 發(fā)送 repo_deduction_msg 消息失敗违孝,對于發(fā)送方 order-service 來說,可能是消息中間件沒有收到消息泳赋;也可能是中間件收到了消息雌桑,但向發(fā)送方 order-service 響應的 ACK 由于網(wǎng)絡故障沒有被 order-service 收到。因此 order-service 貿(mào)然進行事務回滾祖今,撤銷“執(zhí)行插入訂單記錄的查詢語句”校坑,是不對的,因為 repo-service 那邊可能已經(jīng)接收到 repo_deduction_msg 并成功進行了庫存扣減千诬,這樣 order-service 和 repo-service 兩方就產(chǎn)生了數(shù)據(jù)不一致問題耍目。
2)repo-service 和 order-service 把網(wǎng)絡調(diào)用(與 MQ server 通信)放在本地數(shù)據(jù)庫事務里,可能會因為網(wǎng)絡延遲產(chǎn)生數(shù)據(jù)庫長事務徐绑,影響數(shù)據(jù)庫本地事務的并發(fā)度制妄。
以上是被誤解的實現(xiàn)方式,下面給出正確的實現(xiàn)方式泵三,如下所示:
上圖所示的方案,利用消息中間件如 rabbitMQ 來實現(xiàn)分布式下單及庫存扣減過程的最終一致性衔掸。對這幅圖做以下說明:
1)order-service 中烫幕,
在 t_order 表添加訂單記錄 &&
在 t_local_msg 添加對應的扣減庫存消息
這兩個過程要在一個事務中完成,保證過程的原子性敞映。同樣较曼,repo-service 中,
檢查本次扣庫存操作是否已經(jīng)執(zhí)行過 &&
執(zhí)行扣減庫存如果本次扣減操作沒有執(zhí)行過 &&
寫判重表 &&
向 MQ sever 反饋消息消費完成 ACK
這四個過程也要在一個事務中完成振愿,保證過程的原子性捷犹。
2)order-service 中有一個后臺程序弛饭,源源不斷地把消息表中的消息傳送給消息中間件,成功后則刪除消息表中對應的消息萍歉。如果失敗了侣颂,也會不斷嘗試重傳。由于存在網(wǎng)絡 2 將軍問題枪孩,即當 order-service 發(fā)送給消息中間件的消息網(wǎng)絡超時時憔晒,這時候消息中間件可能收到了消息但響應 ACK 失敗,也可能沒收到蔑舞,order-service 會再次發(fā)送該消息拒担,直至消息中間件響應 ACK 成功,這樣可能發(fā)生消息的重復發(fā)送攻询,不過沒關(guān)系从撼,只要保證消息不丟失,不亂序就行钧栖,后面 repo-service 會做去重處理低零。
3)消息中間件向 repo-service 推送 repo_deduction_msg,repo-service 成功處理完成后會向中間件響應 ACK桐经,消息中間件收到這個 ACK 才認為 repo-service 成功處理了這條消息毁兆,否則會重復推送該消息。但是有這樣的情形:repo-service 成功處理了消息阴挣,向中間件發(fā)送的 ACK 在網(wǎng)絡傳輸中由于網(wǎng)絡故障丟失了气堕,導致中間件沒有收到 ACK 重新推送了該消息。這也要靠 repo-service 的消息去重特性來避免消息重復消費畔咧。
4)在 2)和 3)中提到了兩種導致 repo-service 重復收到消息的原因茎芭,一是生產(chǎn)者重復生產(chǎn),二是中間件重傳誓沸。為了實現(xiàn)業(yè)務的冪等性梅桩,repo-service 中維護了一張判重表,這張表中記錄了被成功處理的消息的 id拜隧。repo-service 每次接收到新的消息都先判斷消息是否被成功處理過宿百,若是的話不再重復處理。
通過這種設計洪添,實現(xiàn)了消息在發(fā)送方不丟失垦页,消息在接收方不被重復消費,聯(lián)合起來就是消息不漏不重干奢,嚴格實現(xiàn)了 order-service 和 repo-service 的兩個數(shù)據(jù)庫中數(shù)據(jù)的最終一致性痊焊。
基于消息中間件的最終一致性全局事務方案是互聯(lián)網(wǎng)公司在高并發(fā)場景中探索出的一種創(chuàng)新型應用模式,利用 MQ 實現(xiàn)微服務之間的異步調(diào)用、解耦合和流量削峰薄啥,支持全局事務的高并發(fā)辕羽,并保證分布式數(shù)據(jù)記錄的最終一致性。
三垄惧、Seata in AT mode 的實現(xiàn)
第 2 章給出了實現(xiàn)實現(xiàn)分布式事務的集中常見的理論模型刁愿。本章給出業(yè)界開源分布式事務框架 Seata 的實現(xiàn)。
Seata 為用戶提供了 AT赘艳、TCC酌毡、SAGA 和 XA 事務模式。其中 AT 模式是 Seata 主推的事務模式蕾管,因此本章分析 Seata in AT mode 的實現(xiàn)枷踏。使用 AT 有一個前提,那就是微服務使用的數(shù)據(jù)庫必須是支持事務的關(guān)系型數(shù)據(jù)庫掰曾。
3.1 Seata in AT mode 工作流程概述
Seata 的 AT 模式建立在關(guān)系型數(shù)據(jù)庫的本地事務特性的基礎之上旭蠕,通過數(shù)據(jù)源代理類攔截并解析數(shù)據(jù)庫執(zhí)行的 SQL,記錄自定義的回滾日志旷坦,如需回滾掏熬,則重放這些自定義的回滾日志即可。AT 模式雖然是根據(jù) XA 事務模型(2PC)演進而來的秒梅,但是 AT 打破了 XA 協(xié)議的阻塞性制約旗芬,在一致性和性能上取得了平衡。
AT 模式是基于 XA 事務模型演進而來的捆蜀,它的整體機制也是一個改進版本的兩階段提交協(xié)議疮丛。AT 模式的兩個基本階段是:
1)第一階段:首先獲取本地鎖,執(zhí)行本地事務辆它,業(yè)務數(shù)據(jù)操作和記錄回滾日志在同一個本地事務中提交誊薄,最后釋放本地鎖;
2)第二階段:如需全局提交锰茉,異步刪除回滾日志即可呢蔫,這個過程很快就能完成。如需要回滾飒筑,則通過第一階段的回滾日志進行反向補償片吊。
本章描述 Seata in AT mode 的工作原理使用的電商微服務模型如下圖所示:
在上圖中,協(xié)調(diào)者 shopping-service 先調(diào)用參與者 repo-service 扣減庫存协屡,后調(diào)用參與者 order-service 生成訂單俏脊。這個業(yè)務流使用 Seata in XA mode 后的全局事務流程如下圖所示:
上圖描述的全局事務執(zhí)行流程為:
1)shopping-service 向 Seata 注冊全局事務,并產(chǎn)生一個全局事務標識 XID
2)將 repo-service.repo_db著瓶、order-service.order_db 的本地事務執(zhí)行到待提交階段,事務內(nèi)容包含對 repo-service.repo_db、order-service.order_db 進行的查詢操作以及寫每個庫的 undo_log 記錄
3)repo-service.repo_db材原、order-service.order_db 向 Seata 注冊分支事務沸久,并將其納入該 XID 對應的全局事務范圍
4)提交 repo-service.repo_db、order-service.order_db 的本地事務
5)repo-service.repo_db余蟹、order-service.order_db 向 Seata 匯報分支事務的提交狀態(tài)
6)Seata 匯總所有的 DB 的分支事務的提交狀態(tài)卷胯,決定全局事務是該提交還是回滾
7)Seata 通知 repo-service.repo_db、order-service.order_db 提交/回滾本地事務威酒,若需要回滾窑睁,采取的是補償式方法
其中 1)2)3)4)5)屬于第一階段,6)7)屬于第二階段葵孤。
3.1Seata in AT mode 工作流程詳述
在上面的電商業(yè)務場景中担钮,購物服務調(diào)用庫存服務扣減庫存,調(diào)用訂單服務創(chuàng)建訂單尤仍,顯然這兩個調(diào)用過程要放在一個事務里面箫津。即:
start global_trx
call 庫存服務的扣減庫存接口
call 訂單服務的創(chuàng)建訂單接口
commit global_trx
在庫存服務的數(shù)據(jù)庫中,存在如下的庫存表 t_repo:
在訂單服務的數(shù)據(jù)庫中宰啦,存在如下的訂單表 t_order:
現(xiàn)在苏遥,id 為 40002 的用戶要購買一只商品代碼為 20002 的鼠標,整個分布式事務的內(nèi)容為:
1)在庫存服務的庫存表中將記錄
修改為
2)在訂單服務的訂單表中添加一條記錄
以上操作赡模,在 AT 模式的第一階段的流程圖如下:
從 AT 模式第一階段的流程來看田炭,分支的本地事務在第一階段提交完成之后,就會釋放掉本地事務鎖定的本地記錄漓柑。這是 AT 模式和 XA 最大的不同點教硫,在 XA 事務的兩階段提交中,被鎖定的記錄直到第二階段結(jié)束才會被釋放欺缘。所以 AT 模式減少了鎖記錄的時間栋豫,從而提高了分布式事務的處理效率。AT 模式之所以能夠?qū)崿F(xiàn)第一階段完成就釋放被鎖定的記錄谚殊,是因為 Seata 在每個服務的數(shù)據(jù)庫中維護了一張 undo_log 表丧鸯,其中記錄了對 t_order / t_repo 進行操作前后記錄的鏡像數(shù)據(jù),即便第二階段發(fā)生異常嫩絮,只需回放每個服務的 undo_log 中的相應記錄即可實現(xiàn)全局回滾丛肢。
undo_log 的表結(jié)構(gòu):
第一階段結(jié)束之后,Seata 會接收到所有分支事務的提交狀態(tài)剿干,然后決定是提交全局事務還是回滾全局事務蜂怎。
1)若所有分支事務本地提交均成功,則 Seata 決定全局提交置尔。Seata 將分支提交的消息發(fā)送給各個分支事務杠步,各個分支事務收到分支提交消息后,會將消息放入一個緩沖隊列,然后直接向 Seata 返回提交成功幽歼。之后朵锣,每個本地事務會慢慢處理分支提交消息,處理的方式為:刪除相應分支事務的 undo_log 記錄甸私。之所以只需刪除分支事務的 undo_log 記錄诚些,而不需要再做其他提交操作,是因為提交操作已經(jīng)在第一階段完成了(這也是 AT 和 XA 不同的地方)皇型。這個過程如下圖所示:
分支事務之所以能夠直接返回成功給 Seata诬烹,是因為真正關(guān)鍵的提交操作在第一階段已經(jīng)完成了,清除 undo_log 日志只是收尾工作弃鸦,即便清除失敗了绞吁,也對整個分布式事務不產(chǎn)生實質(zhì)影響。
2)若任一分支事務本地提交失敗寡键,則 Seata 決定全局回滾掀泳,將分支事務回滾消息發(fā)送給各個分支事務,由于在第一階段各個服務的數(shù)據(jù)庫上記錄了 undo_log 記錄西轩,分支事務回滾操作只需根據(jù) undo_log 記錄進行補償即可员舵。全局事務的回滾流程如下圖所示:
這里對圖中的 2、3 步做進一步的說明:
1)由于上文給出了 undo_log 的表結(jié)構(gòu)藕畔,所以可以通過 xid 和 branch_id 來找到當前分支事務的所有 undo_log 記錄马僻;
2)拿到當前分支事務的 undo_log 記錄之后,首先要做數(shù)據(jù)校驗注服,如果 afterImage 中的記錄與當前的表記錄不一致韭邓,說明從第一階段完成到此刻期間,有別的事務修改了這些記錄溶弟,這會導致分支事務無法回滾女淑,向 Seata 反饋回滾失敗辜御;如果 afterImage 中的記錄與當前的表記錄一致鸭你,說明從第一階段完成到此刻期間,沒有別的事務修改這些記錄擒权,分支事務可回滾袱巨,進而根據(jù) beforeImage 和 afterImage 計算出補償 SQL,執(zhí)行補償 SQL 進行回滾碳抄,然后刪除相應 undo_log愉老,向 Seata 反饋回滾成功。
事務具有 ACID 特性剖效,全局事務解決方案也在盡量實現(xiàn)這四個特性嫉入。以上關(guān)于 Seata in AT mode 的描述很顯然體現(xiàn)出了 AT 的原子性焰盗、一致性和持久性。下面著重描述一下 AT 如何保證多個全局事務的隔離性的咒林。
在 AT 中姨谷,當多個全局事務操作同一張表時,通過全局鎖來保證事務的隔離性映九。下面描述一下全局鎖在讀隔離和寫隔離兩個場景中的作用原理:
1)寫隔離(若有全局事務在改/寫/刪記錄,另一個全局事務對同一記錄進行的改/寫/刪要被隔離起來瞎颗,即寫寫互斥):寫隔離是為了在多個全局事務對同一張表的同一個字段進行更新操作時件甥,避免一個全局事務在沒有被提交成功之前所涉及的數(shù)據(jù)被其他全局事務修改。寫隔離的基本原理是:在第一階段本地事務(開啟本地事務的時候哼拔,本地事務會對涉及到的記錄加本地鎖)提交之前引有,確保拿到全局鎖。如果拿不到全局鎖倦逐,就不能提交本地事務譬正,并且不斷嘗試獲取全局鎖,直至超出重試次數(shù)檬姥,放棄獲取全局鎖曾我,回滾本地事務,釋放本地事務對記錄加的本地鎖健民。
假設有兩個全局事務 gtrx_1 和 gtrx_2 在并發(fā)操作庫存服務抒巢,意圖扣減如下記錄的庫存數(shù)量:
AT 實現(xiàn)寫隔離過程的時序圖如下:
圖中,1秉犹、2蛉谜、3、4 屬于第一階段崇堵,5 屬于第二階段型诚。
在上圖中 gtrx_1 和 gtrx_2 均成功提交,如果 gtrx_1 在第二階段執(zhí)行回滾操作鸳劳,那么 gtrx_1 需要重新發(fā)起本地事務獲取本地鎖狰贯,然后根據(jù) undo_log 對這個 id=10002 的記錄進行補償式回滾。此時 gtrx_2 仍在等待全局鎖棍辕,且持有這個 id=10002 的記錄的本地鎖暮现,因此 gtrx_1 會回滾失敗(gtrx_1 回滾需要同時持有全局鎖和對 id=10002 的記錄加的本地鎖)楚昭,回滾失敗的 gtrx_1 會一直重試回滾栖袋。直到旁邊的 gtrx_2 獲取全局鎖的嘗試次數(shù)超過閾值,gtrx_2 會放棄獲取全局鎖抚太,發(fā)起本地回滾塘幅,本地回滾結(jié)束后昔案,自然會釋放掉對這個 id=10002 的記錄加的本地鎖。此時电媳,gtrx_1 終于可以成功對這個 id=10002 的記錄加上了本地鎖踏揣,同時拿到了本地鎖和全局鎖的 gtrx_1 就可以成功回滾了。整個過程匾乓,全局鎖始終在 gtrx_1 手中捞稿,并不會發(fā)生臟寫的問題。整個過程的流程圖如下所示:
2)讀隔離(若有全局事務在改/寫/刪記錄拼缝,另一個全局事務對同一記錄的讀取要被隔離起來娱局,即讀寫互斥):在數(shù)據(jù)庫本地事務的隔離級別為讀已提交、可重復讀咧七、串行化時(讀未提交不起什么隔離作用衰齐,一般不使用),Seata AT 全局事務模型產(chǎn)生的隔離級別是讀未提交继阻,也就是說一個全局事務會看到另一個全局事務未全局提交的數(shù)據(jù)耻涛,產(chǎn)生臟讀,從前文的第一階段和第二階段的流程圖中也可以看出這一點瘟檩。這在最終一致性的分布式事務模型中是可以接受的抹缕。
如果要求 AT 模型一定要實現(xiàn)讀已提交的事務隔離級別,可以利用 Seata 的 SelectForUpdateExecutor 執(zhí)行器對 SELECT FOR UPDATE 語句進行代理墨辛。SELECT FOR UPDATE 語句在執(zhí)行時會申請全局鎖歉嗓,如果全局鎖已經(jīng)被其他全局事務占有,則回滾 SELECT FOR UPDATE 語句的執(zhí)行背蟆,釋放本地鎖鉴分,并且重試 SELECT FOR UPDATE 語句。在這個過程中带膀,查詢請求會被阻塞志珍,直到拿到全局鎖(也就是要讀取的記錄被其他全局事務提交),讀到已被全局事務提交的數(shù)據(jù)才返回垛叨。這個過程如下圖所示:
四伦糯、結(jié)束語
XA 協(xié)議是 X/Open 提出的分布式事務處理標準。文中提到的 2PC嗽元、3PC敛纲、TCC、本地事務表剂癌、Seata in AT mode淤翔,無論哪一種,本質(zhì)都是事務協(xié)調(diào)者協(xié)調(diào)各個事務參與者的本地事務的進度佩谷,使使所有本地事務共同提交或回滾旁壮,最終達成一種全局的 ACID 特性监嗜。在協(xié)調(diào)的過程中,協(xié)調(diào)者需要收集各個本地事務的當前狀態(tài)抡谐,并根據(jù)這些狀態(tài)發(fā)出下一階段的操作指令裁奇。這個思想就是 XA 協(xié)議的要義,我們可以說這些事務模型遵守或大致遵守了 XA 協(xié)議麦撵。
基于消息中間件的最終一致性事務方案是互聯(lián)網(wǎng)公司在高并發(fā)場景中探索出的一種創(chuàng)新型應用模式刽肠,利用 MQ 實現(xiàn)微服務之間的異步調(diào)用、解耦合和流量削峰免胃,保證分布式數(shù)據(jù)記錄的最終一致性五垮。它顯然不遵守 XA 協(xié)議。
對于某項技術(shù)杜秸,可能存在業(yè)界標準或協(xié)議,但實踐者針對具體應用場景的需求或者出于簡便的考慮润绎,給出與標準不完全相符的實現(xiàn)撬碟,甚至完全不相符的實現(xiàn),這在工程領(lǐng)域是一種常見的現(xiàn)象莉撇。TCC 方案如此呢蛤、基于消息中間件的最終一致性事務方案如此、Seata in AT mode 模式也如此棍郎。而新的標準往往就在這些創(chuàng)新中產(chǎn)生其障。
你難道真的沒有發(fā)現(xiàn) 2.6 節(jié)(基于消息中間件的最終一致性事務方案)給出的正確方案中存在的業(yè)務漏洞嗎?請各位重新看下這張圖涂佃,仔細品一品兩個微服務的調(diào)用方向励翼,把你的想法留在評論區(qū)吧 :-)
寫在最后
歡迎大家關(guān)注我的公眾號【風平浪靜如碼】,海量Java相關(guān)文章辜荠,學習資料都會在里面更新汽抚,整理的資料也會放在里面。
覺得寫的還不錯的就點個贊伯病,加個關(guān)注唄造烁!點關(guān)注,不迷路午笛,持續(xù)更新2洋!药磺!