<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          阿里終面:分布式事務(wù)原理

          共 14335字,需瀏覽 29分鐘

           ·

          2020-11-10 20:12

          本文提綱如下

          1. 前言
          2. 單數(shù)據(jù)源事務(wù) & 多數(shù)據(jù)源事務(wù)
          3. 常見分布式事務(wù)解決方案
            • 2.1. 分布式事務(wù)模型
            • 2.2. 二將軍問題和冪等性
            • 2.3. 兩階段提交(2PC) & 三階段提交(3PC)方案
            • 2.4. TCC 方案
            • 2.5. 事務(wù)狀態(tài)表方案
            • 2.6. 基于消息中間件的最終一致性事務(wù)方案
          4. Seata in AT mode 的實現(xiàn)
            • 3.1. Seata in AT mode 工作流程概述
            • 3.2. Seata in AT mode 工作流程詳述
          5. 結(jié)束語
          img

          0. 前言

          從 CPU 到內(nèi)存、到磁盤、到操作系統(tǒng)、到網(wǎng)絡(luò),計算機(jī)系統(tǒng)處處存在不可靠因素。工程師和科學(xué)家努力使用各種軟硬件方法對抗這種不可靠因素,保證數(shù)據(jù)和指令被正確地處理。在網(wǎng)絡(luò)領(lǐng)域有 TCP 可靠傳輸協(xié)議、在存儲領(lǐng)域有 Raid5Raid6 算法、在數(shù)據(jù)庫領(lǐng)域有 基于 ARIES 算法理論實現(xiàn)的事務(wù)機(jī)制……

          這篇文章先介紹單機(jī)數(shù)據(jù)庫事務(wù)的 ACID 特性,然后指出分布式場景下操作多數(shù)據(jù)源面臨的困境,引出分布式系統(tǒng)中常用的分布式事務(wù)解決方案,這些解決方案可以保證業(yè)務(wù)代碼在操作多個數(shù)據(jù)源的時候,能夠像操作單個數(shù)據(jù)源一樣,具備 ACID 特性。

          文章在最后給出業(yè)界較為成熟的分布式事務(wù)框架——Seata 的 AT 模式全局事務(wù)的實現(xiàn)

          1. 單數(shù)據(jù)源事務(wù) & 多數(shù)據(jù)源事務(wù)

          如果一個應(yīng)用程序在一次業(yè)務(wù)流中通過連接驅(qū)動和數(shù)據(jù)源接口只連接并查詢(這里的查詢是廣義的,包括增刪查改等)一個特定的數(shù)據(jù)庫,該應(yīng)用程序就可以利用數(shù)據(jù)庫提供的事務(wù)機(jī)制(如果數(shù)據(jù)庫支持事務(wù)的話)保證對庫中記錄所進(jìn)行的操作的可靠性,這里的可靠性有四種語義:

          • 原子性,A
          • 一致性,C
          • 隔離性,I
          • 持久性,D

          筆者在這里不再對這四種語義進(jìn)行解釋,了解單數(shù)據(jù)源事務(wù)及其 ACID 特性是讀者閱讀這篇文章的前提。單個數(shù)據(jù)庫實現(xiàn)自身的事務(wù)特性是一個復(fù)雜又微妙的過程,例如 MySQL 的 InnoDB 引擎通過 Undo Log + Redo Log + ARIES 算法來實現(xiàn)

          這是一個很宏大的話題,不在本文的描述范圍,讀者有興趣的話可自行研究。

          單數(shù)據(jù)源事務(wù)也可以叫做單機(jī)事務(wù),或者本地事務(wù)。

          在分布式場景下,一個系統(tǒng)由多個子系統(tǒng)構(gòu)成,每個子系統(tǒng)有獨(dú)立的數(shù)據(jù)源。多個子系統(tǒng)之間通過互相調(diào)用來組合出更復(fù)雜的業(yè)務(wù)。

          在時下流行的微服務(wù)系統(tǒng)架構(gòu)中,每一個子系統(tǒng)被稱作一個微服務(wù),同樣每個微服務(wù)都維護(hù)自己的數(shù)據(jù)庫,以保持獨(dú)立性。

          例如,一個電商系統(tǒng)可能由購物微服務(wù)、庫存微服務(wù)、訂單微服務(wù)等組成。購物微服務(wù)通過調(diào)用庫存微服務(wù)和訂單微服務(wù)來整合出購物業(yè)務(wù)。用戶請求購物微服務(wù)商完成下單時,購物微服務(wù)一方面調(diào)用庫存微服務(wù)扣減相應(yīng)商品的庫存數(shù)量,另一方面調(diào)用訂單微服務(wù)插入訂單記錄(為了后文描述分布式事務(wù)解決方案的方便,這里給出的是一個最簡單的電商系統(tǒng)微服務(wù)劃分和最簡單的購物業(yè)務(wù)流程,后續(xù)的支付、物流等業(yè)務(wù)不在考慮范圍內(nèi))。電商系統(tǒng)模型如下圖所示:

          img

          在用戶購物的業(yè)務(wù)場景中,shopping-service 的業(yè)務(wù)涉及兩個數(shù)據(jù)庫:庫存庫(repo_db)訂單庫(repo_db),也就是 g 購物業(yè)務(wù)是調(diào)用多數(shù)據(jù)源來組合而成的。作為一個面向消費(fèi)者的系統(tǒng),電商系統(tǒng)要保證購物業(yè)務(wù)的高度可靠性,這里的可靠性同樣有 ACID 四種語義。

          但是一個數(shù)據(jù)庫的本地事務(wù)機(jī)制僅僅對落到自己身上的查詢操作(這里的查詢是廣義的,包括增刪改查等)起作用,無法干涉對其他數(shù)據(jù)庫的查詢操作。所以,數(shù)據(jù)庫自身提供的本地事務(wù)機(jī)制無法確保業(yè)務(wù)對多數(shù)據(jù)源全局操作的可靠性。

          基于此,針對多數(shù)據(jù)源操作提出的分布式事務(wù)機(jī)制就出現(xiàn)了。

          分布式事務(wù)也可以叫做全局事務(wù)。

          2. 常見分布式事務(wù)解決方案

          2.1. 分布式事務(wù)模型

          img

          描述分布式事務(wù),常常會使用以下幾個名詞:

          • 事務(wù)參與者:例如每個數(shù)據(jù)庫就是一個事務(wù)參與者
          • 事務(wù)協(xié)調(diào)者:訪問多個數(shù)據(jù)源的服務(wù)程序,例如 shopping-service 就是事務(wù)協(xié)調(diào)者
          • 資源管理器(Resource Manager, RM):通常與事務(wù)參與者同義
          • 事務(wù)管理器(Transaction Manager, TM):通常與事務(wù)協(xié)調(diào)者同義

          在分布式事務(wù)模型中,一個 TM 管理多個 RM,即一個服務(wù)程序訪問多個數(shù)據(jù)源;TM 是一個全局事務(wù)管理器,協(xié)調(diào)多方本地事務(wù)的進(jìn)度,使其共同提交或回滾,最終達(dá)成一種全局的 ACID 特性。

          2.2. 二將軍問題和冪等性

          二將軍問題是網(wǎng)絡(luò)領(lǐng)域的一個經(jīng)典問題,用于表達(dá)計算機(jī)網(wǎng)絡(luò)中互聯(lián)協(xié)議設(shè)計的微妙性和復(fù)雜性。

          這里給出一個二將軍問題的簡化版本:

          一支白軍被圍困在一個山谷中,山谷的左右兩側(cè)是藍(lán)軍。困在山谷中的白軍人數(shù)多于山谷兩側(cè)的任意一支藍(lán)軍,而少于兩支藍(lán)軍的之和。若一支藍(lán)軍對白軍單獨(dú)發(fā)起進(jìn)攻,則必敗無疑;但若兩支藍(lán)軍同時發(fā)起進(jìn)攻,則可取勝。兩只藍(lán)軍的總指揮位于山谷左側(cè),他希望兩支藍(lán)軍同時發(fā)起進(jìn)攻,這樣就要把命令傳到山谷右側(cè)的藍(lán)軍,以告知發(fā)起進(jìn)攻的具體時間。假設(shè)他們只能派遣士兵穿越白軍所在的山谷(唯一的通信信道)來傳遞消息,那么在穿越山谷時,士兵有可能被俘虜。

          img

          只有當(dāng)送信士兵成功往返后,總指揮才能確認(rèn)這場戰(zhàn)爭的勝利(上方圖)。現(xiàn)在問題來了,派遣出去送信的士兵沒有回來,則左側(cè)藍(lán)軍中的總指揮能不能決定按命令中約定的時間發(fā)起進(jìn)攻?

          答案是不確定,派遣出去送信的士兵沒有回來,他可能遇到兩種狀況:

          • 命令還沒送達(dá)就被俘虜了(中間圖),這時候右側(cè)藍(lán)軍根本不知道要何時進(jìn)攻;
          • 命令送達(dá),但返回途中被俘虜了(下方圖),這時候右側(cè)藍(lán)軍知道要何時進(jìn)攻,但左側(cè)藍(lán)軍不知道右側(cè)藍(lán)軍是否知曉進(jìn)攻時間。

          類似的問題在計算機(jī)網(wǎng)絡(luò)中普遍存在,例如發(fā)送者給接受者發(fā)送一個 HTTP 請求,或者 MySQL 客戶端向 MySQL 服務(wù)器發(fā)送一條插入語句,然后超時了沒有得到響應(yīng)。請問服務(wù)器是寫入成功了還是失敗了?答案是不確定,有以下幾種情況:

          • 可能請求由于網(wǎng)絡(luò)故障根本沒有送到服務(wù)器,因此寫入失敗;
          • 可能服務(wù)器收到了,也寫入成功了,但是向客戶端發(fā)送響應(yīng)前服務(wù)器宕機(jī)了;
          • 可能服務(wù)器收到了,也寫入成功了,也向客戶端發(fā)送了響應(yīng),但是由于網(wǎng)絡(luò)故障未送到客戶端。

          無論哪種場景,在客戶端看來都是一樣的結(jié)果:它發(fā)出的請求沒有得到響應(yīng)。為了確保服務(wù)端成功寫入數(shù)據(jù),客戶端只能重發(fā)請求,直至接收到服務(wù)端的響應(yīng)。

          類似的問題問題被稱為網(wǎng)絡(luò)二將軍問題

          網(wǎng)絡(luò)二將軍問題的存在使得消息的發(fā)送者往往要重復(fù)發(fā)送消息,直到收到接收者的確認(rèn)才認(rèn)為發(fā)送成功,但這往往又會導(dǎo)致消息的重復(fù)發(fā)送。 例如電商系統(tǒng)中訂單模塊調(diào)用支付模塊扣款的時候,如果網(wǎng)絡(luò)故障導(dǎo)致二將軍問題出現(xiàn),扣款請求重復(fù)發(fā)送,產(chǎn)生的重復(fù)扣款結(jié)果顯然是不能被接受的。因此要保證一次事務(wù)中的扣款請求無論被發(fā)送多少次,接收方有且只執(zhí)行一次扣款動作,這種保證機(jī)制叫做接收方的冪等性。

          img

          2.3. 兩階段提交(2PC) & 三階段提交(3PC)方案

          2PC 是一種實現(xiàn)分布式事務(wù)的簡單模型,這兩個階段是:

          • 準(zhǔn)備階段:事務(wù)協(xié)調(diào)者向各個事務(wù)參與者發(fā)起詢問請求:“我要執(zhí)行全局事務(wù)了,這個事務(wù)涉及到的資源分布在你們這些數(shù)據(jù)源中,分別是……,你們準(zhǔn)備好各自的資源(即各自執(zhí)行本地事務(wù)到待提交階段)”。各個參與者協(xié)調(diào)者回復(fù) yes(表示已準(zhǔn)備好,允許提交全局事務(wù))或 no(表示本參與者無法拿到全局事務(wù)所需的本地資源,因為它被其他本地事務(wù)鎖住了)或超時。
          • 提交階段:如果各個參與者回復(fù)的都是 yes,則協(xié)調(diào)者向所有參與者發(fā)起事務(wù)提交操作,然后所有參與者收到后各自執(zhí)行本地事務(wù)提交操作并向協(xié)調(diào)者發(fā)送 ACK;如果任何一個參與者回復(fù) no 或者超時,則協(xié)調(diào)者向所有參與者發(fā)起事務(wù)回滾操作,然后所有參與者收到后各自執(zhí)行本地事務(wù)回滾操作并向協(xié)調(diào)者發(fā)送 ACK。

          2PC 的流程如下圖所示:

          img

          從上圖可以看出,要實現(xiàn) 2PC,所有的參與者都要實現(xiàn)三個接口:

          • Prepare():TM 調(diào)用該接口詢問各個本地事務(wù)是否就緒
          • Commit():TM 調(diào)用該接口要求各個本地事務(wù)提交
          • Rollback():TM 調(diào)用該接口要求各個本地事務(wù)回滾

          可以將這三個接口簡單地(但不嚴(yán)謹(jǐn)?shù)兀├斫獬?XA 協(xié)議。XA 協(xié)議是 X/Open 提出的分布式事務(wù)處理標(biāo)準(zhǔn)。MySQL、Oracle、DB2 這些主流數(shù)據(jù)庫都實現(xiàn)了 XA 協(xié)議,因此都能被用于實現(xiàn) 2PC 事務(wù)模型。

          2PC 簡明易懂,但存在如下的問題:

          • 性能差。在準(zhǔn)備階段,要等待所有的參與者返回,才能進(jìn)入階段二,在這期間,各個參與者上面的相關(guān)資源被排他地鎖住,參與者上面意圖使用這些資源的本地事務(wù)只能等待。因為存在這種同步阻塞問題,所以影響了各個參與者的本地事務(wù)并發(fā)度;
          • 準(zhǔn)備階段完成后,如果協(xié)調(diào)者宕機(jī),所有的參與者都收不到提交或回滾指令,導(dǎo)致所有參與者“不知所措”;
          • 在提交階段,協(xié)調(diào)者向所有的參與者發(fā)送了提交指令,如果一個參與者未返回 ACK,那么協(xié)調(diào)者不知道這個參與者內(nèi)部發(fā)生了什么(由于網(wǎng)絡(luò)二將軍問題的存在,這個參與者可能根本沒收到提交指令,一直處于等待接收提交指令的狀態(tài);也可能收到了,并成功執(zhí)行了本地提交,但返回的 ACK 由于網(wǎng)絡(luò)故障未送到協(xié)調(diào)者上),也就無法決定下一步是否進(jìn)行全體參與者的回滾。

          2PC 之后又出現(xiàn)了 3PC,把兩階段過程變成了三階段過程,分別是:1.詢問階段2.準(zhǔn)備階段3.提交或回滾階段,這里不再詳述。

          3PC 利用超時機(jī)制解決了 2PC 的同步阻塞問題,避免資源被永久鎖定,進(jìn)一步加強(qiáng)了整個事務(wù)過程的可靠性。但是 3PC 同樣無法應(yīng)對類似的宕機(jī)問題,只不過出現(xiàn)多數(shù)據(jù)源中數(shù)據(jù)不一致問題的概率更小。

          2PC 除了性能和可靠性上存在問題,它的適用場景也很局限,它要求參與者實現(xiàn)了 XA 協(xié)議,例如使用實現(xiàn)了 XA 協(xié)議的數(shù)據(jù)庫作為參與者可以完成 2PC 過程。但是在多個系統(tǒng)服務(wù)利用 api 接口相互調(diào)用的時候,就不遵守 XA 協(xié)議了,這時候 2PC 就不適用了。所以 2PC 在分布式應(yīng)用場景中很少使用。

          所以前文提到的電商場景無法使用 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 方案使用的電商微服務(wù)模型如下圖所示,在這個模型中,shopping-service 是事務(wù)協(xié)調(diào)者,repo-service 和 order-service 是事務(wù)參與者。

          img

          上文提到,2PC 要求參與者實現(xiàn)了 XA 協(xié)議,通常用來解決多個數(shù)據(jù)庫之間的事務(wù)問題,比較局限。在多個系統(tǒng)服務(wù)利用 api 接口相互調(diào)用的時候,就不遵守 XA 協(xié)議了,這時候 2PC 就不適用了。現(xiàn)代企業(yè)多采用分布式的微服務(wù),因此更多的是要解決多個微服務(wù)之間的分布式事務(wù)問題。

          TCC 就是一種解決多個微服務(wù)之間的分布式事務(wù)問題的方案。TCC 是 Try、Confirm、Cancel 三個詞的縮寫,其本質(zhì)是一個應(yīng)用層面上的 2PC,同樣分為兩個階段:

          • 準(zhǔn)備階段 :協(xié)調(diào)者調(diào)用所有的每個微服務(wù)提供的 try 接口,將整個全局事務(wù)涉及到的資源鎖定住,若鎖定成功 try 接口向協(xié)調(diào)者返回 yes。
          • 提交階段 :若所有的服務(wù)的 try 接口在階段一都返回 yes,則進(jìn)入提交階段,協(xié)調(diào)者調(diào)用所有服務(wù)的 confirm 接口,各個服務(wù)進(jìn)行事務(wù)提交。如果有任何一個服務(wù)的 try 接口在階段一返回 no 或者超時,則協(xié)調(diào)者調(diào)用所有服務(wù)的 cancel 接口。

          TCC 的流程如下圖所示:

          img

          這里有個關(guān)鍵問題,既然 TCC 是一種服務(wù)層面上的 2PC。它是如何解決 2PC 無法應(yīng)對宕機(jī)問題的缺陷的呢?

          答案是不斷重試。

          由于 try 操作鎖住了全局事務(wù)涉及的所有資源,保證了業(yè)務(wù)操作的所有前置條件得到滿足,因此無論是 confirm 階段失敗還是 cancel 階段失敗都能通過不斷重試直至 confirm 或 cancel 成功(所謂成功就是所有的服務(wù)都對 confirm 或者 cancel 返回了 ACK)。

          img

          這里還有個關(guān)鍵問題,在不斷重試 confirm 和 cancel 的過程中(考慮到網(wǎng)絡(luò)二將軍問題的存在)有可能重復(fù)進(jìn)行了 confirm 或 cancel,因此還要再保證 confirm 和 cancel 操作具有冪等性,也就是整個全局事務(wù)中,每個參與者只進(jìn)行一次 confirm 或者 cancel。實現(xiàn) confirm 和 cancel 操作的冪等性,有很多解決方案,例如每個參與者可以維護(hù)一個去重表(可以利用數(shù)據(jù)庫表實現(xiàn)也可以使用內(nèi)存型 KV 組件實現(xiàn)),記錄每個全局事務(wù)(以全局事務(wù)標(biāo)記 XID 區(qū)分)是否進(jìn)行過 confirm 或 cancel 操作,若已經(jīng)進(jìn)行過,則不再重復(fù)執(zhí)行。

          TCC 由支付寶團(tuán)隊提出,被廣泛應(yīng)用于金融系統(tǒng)中。我們用銀行賬戶余額購買基金時,會注意到銀行賬戶中用于購買基金的那部分余額首先會被凍結(jié),由此我們可以猜想,這個過程大概就是 TCC 的第一階段。

          2.5. 事務(wù)狀態(tài)表方案

          另外有一種類似 TCC 的事務(wù)解決方案,借助事務(wù)狀態(tài)表來實現(xiàn)。假設(shè)要在一個分布式事務(wù)中實現(xiàn)調(diào)用 repo-service 扣減庫存、調(diào)用 order-service 生成訂單兩個過程。在這種方案中,協(xié)調(diào)者 shopping-service 維護(hù)一張如下的事務(wù)狀態(tài)表:

          分布式事務(wù) ID事務(wù)內(nèi)容事務(wù)狀態(tài)
          global_trx_id_1操作 1:調(diào)用 repo-service 扣減庫存 操作 2:調(diào)用 order-service 生成訂單狀態(tài) 1:初始 狀態(tài) 2:操作 1 成功 狀態(tài) 3:操作 1、2 成功

          初始狀態(tài)為 1,每成功調(diào)用一個服務(wù)則更新一次狀態(tài),最后所有的服務(wù)調(diào)用成功,狀態(tài)更新到 3。

          有了這張表,就可以啟動一個后臺任務(wù),掃描這張表中事務(wù)的狀態(tài),如果一個分布式事務(wù)一直(設(shè)置一個事務(wù)周期閾值)未到狀態(tài) 3,說明這條事務(wù)沒有成功執(zhí)行,于是可以重新調(diào)用 repo-service 扣減庫存、調(diào)用 order-service 生成訂單。直至所有的調(diào)用成功,事務(wù)狀態(tài)到 3。

          如果多次重試仍未使得狀態(tài)到 3,可以將事務(wù)狀態(tài)置為 error,通過人工介入進(jìn)行干預(yù)。

          由于存在服務(wù)的調(diào)用重試,因此每個服務(wù)的接口要根據(jù)全局的分布式事務(wù) ID 做冪等,原理同 2.4 節(jié)的冪等性實現(xiàn)。

          2.6. 基于消息中間件的最終一致性事務(wù)方案

          無論是 2PC & 3PC 還是 TCC、事務(wù)狀態(tài)表,基本都遵守 XA 協(xié)議的思想。即這些方案本質(zhì)上都是事務(wù)協(xié)調(diào)者協(xié)調(diào)各個事務(wù)參與者的本地事務(wù)的進(jìn)度,使所有本地事務(wù)共同提交或回滾,最終達(dá)成一種全局的 ACID 特性。在協(xié)調(diào)的過程中,協(xié)調(diào)者需要收集各個本地事務(wù)的當(dāng)前狀態(tài),并根據(jù)這些狀態(tài)發(fā)出下一階段的操作指令。

          但是這些全局事務(wù)方案由于操作繁瑣、時間跨度大,或者在全局事務(wù)期間會排他地鎖住相關(guān)資源,使得整個分布式系統(tǒng)的全局事務(wù)的并發(fā)度不會太高。這很難滿足電商等高并發(fā)場景對事務(wù)吞吐量的要求,因此互聯(lián)網(wǎng)服務(wù)提供商探索出了很多與 XA 協(xié)議背道而馳的分布式事務(wù)解決方案。其中利用消息中間件實現(xiàn)的最終一致性全局事務(wù)就是一個經(jīng)典方案。

          img

          為了表現(xiàn)出這種方案的精髓,我將使用如下的電商系統(tǒng)微服務(wù)結(jié)構(gòu)來進(jìn)行描述:

          img

          在這個模型中,用戶不再是請求整合后的 shopping-service 進(jìn)行下單,而是直接請求 order-service 下單,order-service 一方面添加訂單記錄,另一方面會調(diào)用 repo-service 扣減庫存。

          這種基于消息中間件的最終一致性事務(wù)方案常常被誤解成如下的實現(xiàn)方式:

          img

          這種實現(xiàn)方式的流程是:

          1. order-service 負(fù)責(zé)向 MQ server 發(fā)送扣減庫存消息(repo_deduction_msg);repo-service 訂閱 MQ server 中的扣減庫存消息,負(fù)責(zé)消費(fèi)消息。
          2. 用戶下單后,order-service 先執(zhí)行插入訂單記錄的查詢語句,后將 repo_deduction_msg 發(fā)到消息中間件中,這兩個過程放在一個本地事務(wù)中進(jìn)行,一旦“執(zhí)行插入訂單記錄的查詢語句”失敗,導(dǎo)致事務(wù)回滾,“將 repo_deduction_msg 發(fā)到消息中間件中”就不會發(fā)生;同樣,一旦“將 repo_deduction_msg 發(fā)到消息中間件中”失敗,拋出異常,也會導(dǎo)致“執(zhí)行插入訂單記錄的查詢語句”操作回滾,最終什么也沒有發(fā)生。
          img
          1. repo-service 接收到 repo_deduction_msg 之后,先執(zhí)行庫存扣減查詢語句,后向 MQ sever 反饋消息消費(fèi)完成 ACK,這兩個過程放在一個本地事務(wù)中進(jìn)行,一旦“執(zhí)行庫存扣減查詢語句”失敗,導(dǎo)致事務(wù)回滾,“向 MQ sever 反饋消息消費(fèi)完成 ACK”就不會發(fā)生,MQ server 在 Confirm 機(jī)制的驅(qū)動下會繼續(xù)向 repo-service 推送該消息,直到整個事務(wù)成功提交;同樣,一旦“向 MQ sever 反饋消息消費(fèi)完成 ACK”失敗,拋出異常,也對導(dǎo)致“執(zhí)行庫存扣減查詢語句”操作回滾,MQ server 在 Confirm 機(jī)制的驅(qū)動下會繼續(xù)向 repo-service 推送該消息,直到整個事務(wù)成功提交。
          img

          這種做法看似很可靠。但沒有考慮到網(wǎng)絡(luò)二將軍問題的存在,有如下的缺陷:

          • 網(wǎng)絡(luò)的 2 將軍問題 :上面第 2 步中 order-service 發(fā)送 repo_deduction_msg 消息失敗,對于發(fā)送方 order-service 來說,可能是消息中間件沒有收到消息;也可能是中間件收到了消息,但向發(fā)送方 order-service 響應(yīng)的 ACK 由于網(wǎng)絡(luò)故障沒有被 order-service 收到。因此 order-service 貿(mào)然進(jìn)行事務(wù)回滾,撤銷“執(zhí)行插入訂單記錄的查詢語句”,是不對的,因為 repo-service 那邊可能已經(jīng)接收到 repo_deduction_msg 并成功進(jìn)行了庫存扣減,這樣 order-service 和 repo-service 兩方就產(chǎn)生了數(shù)據(jù)不一致問題。
          • 數(shù)據(jù)庫長事務(wù)問題 :repo-service 和 order-service 把網(wǎng)絡(luò)調(diào)用(與 MQ server 通信)放在本地數(shù)據(jù)庫事務(wù)里,可能會因為網(wǎng)絡(luò)延遲產(chǎn)生數(shù)據(jù)庫長事務(wù),影響數(shù)據(jù)庫本地事務(wù)的并發(fā)度。
          img

          以上是被誤解的實現(xiàn)方式,下面給出正確的實現(xiàn)方式,如下所示:

          img

          上圖所示的方案,利用消息中間件如 rabbitMQ 來實現(xiàn)分布式下單及庫存扣減過程的最終一致性。對這幅圖做以下說明:

          1)order-service 中,

          在 t_order 表添加訂單記錄 &&

          在 t_local_msg 添加對應(yīng)的扣減庫存消息

          這兩個過程要在一個事務(wù)中完成,保證過程的原子性。同樣,repo-service 中,

          檢查本次扣庫存操作是否已經(jīng)執(zhí)行過 &&

          執(zhí)行扣減庫存如果本次扣減操作沒有執(zhí)行過 &&

          寫判重表 &&

          向 MQ sever 反饋消息消費(fèi)完成 ACK

          這四個過程也要在一個事務(wù)中完成,保證過程的原子性。

          2)order-service 中有一個后臺程序,源源不斷地把消息表中的消息傳送給消息中間件,成功后則刪除消息表中對應(yīng)的消息。如果失敗了,也會不斷嘗試重傳。由于存在網(wǎng)絡(luò) 2 將軍問題,即當(dāng) order-service 發(fā)送給消息中間件的消息網(wǎng)絡(luò)超時時,這時候消息中間件可能收到了消息但響應(yīng) ACK 失敗,也可能沒收到,order-service 會再次發(fā)送該消息,直至消息中間件響應(yīng) ACK 成功,這樣可能發(fā)生消息的重復(fù)發(fā)送,不過沒關(guān)系,只要保證消息不丟失,不亂序就行,后面 repo-service 會做去重處理。

          3)消息中間件向 repo-service 推送 repo_deduction_msg,repo-service 成功處理完成后會向中間件響應(yīng) ACK,消息中間件收到這個 ACK 才認(rèn)為 repo-service 成功處理了這條消息,否則會重復(fù)推送該消息。但是有這樣的情形:repo-service 成功處理了消息,向中間件發(fā)送的 ACK 在網(wǎng)絡(luò)傳輸中由于網(wǎng)絡(luò)故障丟失了,導(dǎo)致中間件沒有收到 ACK 重新推送了該消息。這也要靠 repo-service 的消息去重特性來避免消息重復(fù)消費(fèi)。

          4)在 2)和 3)中提到了兩種導(dǎo)致 repo-service 重復(fù)收到消息的原因,一是生產(chǎn)者重復(fù)生產(chǎn),二是中間件重傳。為了實現(xiàn)業(yè)務(wù)的冪等性,repo-service 中維護(hù)了一張判重表,這張表中記錄了被成功處理的消息的 id。repo-service 每次接收到新的消息都先判斷消息是否被成功處理過,若是的話不再重復(fù)處理。

          img

          通過這種設(shè)計,實現(xiàn)了消息在發(fā)送方不丟失,消息在接收方不被重復(fù)消費(fèi),聯(lián)合起來就是消息不漏不重,嚴(yán)格實現(xiàn)了 order-service 和 repo-service 的兩個數(shù)據(jù)庫中數(shù)據(jù)的最終一致性。

          基于消息中間件的最終一致性全局事務(wù)方案是互聯(lián)網(wǎng)公司在高并發(fā)場景中探索出的一種創(chuàng)新型應(yīng)用模式,利用 MQ 實現(xiàn)微服務(wù)之間的異步調(diào)用、解耦合和流量削峰,支持全局事務(wù)的高并發(fā),并保證分布式數(shù)據(jù)記錄的最終一致性。

          img

          3. Seata in AT mode 的實現(xiàn)

          第 2 章給出了實現(xiàn)實現(xiàn)分布式事務(wù)的集中常見的理論模型。本章給出業(yè)界開源分布式事務(wù)框架 Seata 的實現(xiàn)。

          Seata 為用戶提供了 AT、TCC、SAGA 和 XA 事務(wù)模式。其中 AT 模式是 Seata 主推的事務(wù)模式,因此本章分析 Seata in AT mode 的實現(xiàn)。使用 AT 有一個前提,那就是微服務(wù)使用的數(shù)據(jù)庫必須是支持事務(wù)的關(guān)系型數(shù)據(jù)庫。

          3.1. Seata in AT mode 工作流程概述

          Seata 的 AT 模式建立在關(guān)系型數(shù)據(jù)庫的本地事務(wù)特性的基礎(chǔ)之上,通過數(shù)據(jù)源代理類攔截并解析數(shù)據(jù)庫執(zhí)行的 SQL,記錄自定義的回滾日志,如需回滾,則重放這些自定義的回滾日志即可。AT 模式雖然是根據(jù) XA 事務(wù)模型(2PC)演進(jìn)而來的,但是 AT 打破了 XA 協(xié)議的阻塞性制約,在一致性和性能上取得了平衡。

          AT 模式是基于 XA 事務(wù)模型演進(jìn)而來的,它的整體機(jī)制也是一個改進(jìn)版本的兩階段提交協(xié)議。AT 模式的兩個基本階段是:

          • 首先獲取本地鎖,執(zhí)行本地事務(wù),業(yè)務(wù)數(shù)據(jù)操作和記錄回滾日志在同一個本地事務(wù)中提交,最后釋放本地鎖;
          • 如需全局提交,異步刪除回滾日志即可,這個過程很快就能完成。如需要回滾,則通過第一階段的回滾日志進(jìn)行反向補(bǔ)償。

          本章描述 Seata in AT mode 的工作原理使用的電商微服務(wù)模型如下圖所示:

          img

          在上圖中,協(xié)調(diào)者 shopping-service 先調(diào)用參與者 repo-service 扣減庫存,后調(diào)用參與者 order-service 生成訂單。這個業(yè)務(wù)流使用 Seata in XA mode 后的全局事務(wù)流程如下圖所示:

          img

          上圖描述的全局事務(wù)執(zhí)行流程為:

          1. shopping-service 向 Seata 注冊全局事務(wù),并產(chǎn)生一個全局事務(wù)標(biāo)識 XID
          2. 將 repo-service.repo_db、order-service.order_db 的本地事務(wù)執(zhí)行到待提交階段,事務(wù)內(nèi)容包含對 repo-service.repo_db、order-service.order_db 進(jìn)行的查詢操作以及寫每個庫的 undo_log 記錄
          3. repo-service.repo_db、order-service.order_db 向 Seata 注冊分支事務(wù),并將其納入該 XID 對應(yīng)的全局事務(wù)范圍
          4. 提交 repo-service.repo_db、order-service.order_db 的本地事務(wù)
          5. repo-service.repo_db、order-service.order_db 向 Seata 匯報分支事務(wù)的提交狀態(tài)
          6. Seata 匯總所有的 DB 的分支事務(wù)的提交狀態(tài),決定全局事務(wù)是該提交還是回滾
          7. Seata 通知 repo-service.repo_db、order-service.order_db 提交/回滾本地事務(wù),若需要回滾,采取的是補(bǔ)償式方法

          其中 1)2)3)4)5)屬于第一階段,6)7)屬于第二階段。

          3.2. Seata in AT mode 工作流程詳述

          在上面的電商業(yè)務(wù)場景中,購物服務(wù)調(diào)用庫存服務(wù)扣減庫存,調(diào)用訂單服務(wù)創(chuàng)建訂單,顯然這兩個調(diào)用過程要放在一個事務(wù)里面。即:

          start global_trx

          call 庫存服務(wù)的扣減庫存接口

          call 訂單服務(wù)的創(chuàng)建訂單接口

          commit global_trx

          在庫存服務(wù)的數(shù)據(jù)庫中,存在如下的庫存表 t_repo:

          idproduction_codenamecountprice
          1000120001xx 鍵盤98200.0
          1000220002yy 鼠標(biāo)199100.0

          在訂單服務(wù)的數(shù)據(jù)庫中,存在如下的訂單表 t_order:

          idorder_codeuser_idproduction_codecountprice
          30001202010250000140001200021100.0
          30002202010250000140001200012400.0

          現(xiàn)在,id 為 40002 的用戶要購買一只商品代碼為 20002 的鼠標(biāo),整個分布式事務(wù)的內(nèi)容為:

          1)在庫存服務(wù)的庫存表中將記錄

          idproduction_codenamecountprice
          1000220002yy 鼠標(biāo)199100.0

          修改為

          idproduction_codenamecountprice
          1000220002yy 鼠標(biāo)198100.0

          2)在訂單服務(wù)的訂單表中添加一條記錄

          idorder_codeuser_idproduction_codecountprice
          30003202010250000240002200021100.0

          以上操作,在 AT 模式的第一階段的流程圖如下:

          img

          從 AT 模式第一階段的流程來看,分支的本地事務(wù)在第一階段提交完成之后,就會釋放掉本地事務(wù)鎖定的本地記錄。這是 AT 模式和 XA 最大的不同點(diǎn),在 XA 事務(wù)的兩階段提交中,被鎖定的記錄直到第二階段結(jié)束才會被釋放。所以 AT 模式減少了鎖記錄的時間,從而提高了分布式事務(wù)的處理效率

          AT 模式之所以能夠?qū)崿F(xiàn)第一階段完成就釋放被鎖定的記錄,是因為 Seata 在每個服務(wù)的數(shù)據(jù)庫中維護(hù)了一張 undo_log 表,其中記錄了對 t_order / t_repo 進(jìn)行操作前后記錄的鏡像數(shù)據(jù),即便第二階段發(fā)生異常,只需回放每個服務(wù)的 undo_log 中的相應(yīng)記錄即可實現(xiàn)全局回滾。

          undo_log 的表結(jié)構(gòu):

          idbranch_idxidcontextrollback_infolog_statuslog_createdlog_modified
          ……分支事務(wù) ID全局事務(wù) ID……分支事務(wù)操作的記錄在事務(wù)前后的記錄鏡像,即 beforeImage 和 afterImage………………

          第一階段結(jié)束之后,Seata 會接收到所有分支事務(wù)的提交狀態(tài),然后決定是提交全局事務(wù)還是回滾全局事務(wù)。

          1)若所有分支事務(wù)本地提交均成功,則 Seata 決定全局提交。 Seata 將分支提交的消息發(fā)送給各個分支事務(wù),各個分支事務(wù)收到分支提交消息后,會將消息放入一個緩沖隊列,然后直接向 Seata 返回提交成功。之后,每個本地事務(wù)會慢慢處理分支提交消息,處理的方式為:刪除相應(yīng)分支事務(wù)的 undo_log 記錄。之所以只需刪除分支事務(wù)的 undo_log 記錄,而不需要再做其他提交操作,是因為提交操作已經(jīng)在第一階段完成了(這也是 AT 和 XA 不同的地方)。這個過程如下圖所示:

          img

          分支事務(wù)之所以能夠直接返回成功給 Seata,是因為真正關(guān)鍵的提交操作在第一階段已經(jīng)完成了,清除 undo_log 日志只是收尾工作,即便清除失敗了,也對整個分布式事務(wù)不產(chǎn)生實質(zhì)影響。

          2)若任一分支事務(wù)本地提交失敗,則 Seata 決定全局回滾,將分支事務(wù)回滾消息發(fā)送給各個分支事務(wù),由于在第一階段各個服務(wù)的數(shù)據(jù)庫上記錄了 undo_log 記錄,分支事務(wù)回滾操作只需根據(jù) undo_log 記錄進(jìn)行補(bǔ)償即可。全局事務(wù)的回滾流程如下圖所示:

          img

          這里對圖中的 2、3 步做進(jìn)一步的說明:

          • 由于上文給出了 undo_log 的表結(jié)構(gòu),所以可以通過 xid 和 branch_id 來找到當(dāng)前分支事務(wù)的所有 undo_log 記錄;
          • 拿到當(dāng)前分支事務(wù)的 undo_log 記錄之后,首先要做數(shù)據(jù)校驗,如果 afterImage 中的記錄與當(dāng)前的表記錄不一致,說明從第一階段完成到此刻期間,有別的事務(wù)修改了這些記錄,這會導(dǎo)致分支事務(wù)無法回滾,向 Seata 反饋回滾失敗;如果 afterImage 中的記錄與當(dāng)前的表記錄一致,說明從第一階段完成到此刻期間,沒有別的事務(wù)修改這些記錄,分支事務(wù)可回滾,進(jìn)而根據(jù) beforeImage 和 afterImage 計算出補(bǔ)償 SQL,執(zhí)行補(bǔ)償 SQL 進(jìn)行回滾,然后刪除相應(yīng) undo_log,向 Seata 反饋回滾成功。
          img

          事務(wù)具有 ACID 特性,全局事務(wù)解決方案也在盡量實現(xiàn)這四個特性。以上關(guān)于 Seata in AT mode 的描述很顯然體現(xiàn)出了 AT 的原子性、一致性和持久性。下面著重描述一下 AT 如何保證多個全局事務(wù)的隔離性的。

          在 AT 中,當(dāng)多個全局事務(wù)操作同一張表時,通過全局鎖來保證事務(wù)的隔離性。下面描述一下全局鎖在讀隔離和寫隔離兩個場景中的作用原理:

          1)寫隔離(若有全局事務(wù)在改/寫/刪記錄,另一個全局事務(wù)對同一記錄進(jìn)行的改/寫/刪要被隔離起來,即寫寫互斥):寫隔離是為了在多個全局事務(wù)對同一張表的同一個字段進(jìn)行更新操作時,避免一個全局事務(wù)在沒有被提交成功之前所涉及的數(shù)據(jù)被其他全局事務(wù)修改。寫隔離的基本原理是:在第一階段本地事務(wù)(開啟本地事務(wù)的時候,本地事務(wù)會對涉及到的記錄加本地鎖)提交之前,確保拿到全局鎖。如果拿不到全局鎖,就不能提交本地事務(wù),并且不斷嘗試獲取全局鎖,直至超出重試次數(shù),放棄獲取全局鎖,回滾本地事務(wù),釋放本地事務(wù)對記錄加的本地鎖。

          假設(shè)有兩個全局事務(wù) gtrx_1 和 gtrx_2 在并發(fā)操作庫存服務(wù),意圖扣減如下記錄的庫存數(shù)量:

          idproduction_codenamecountprice
          1000220002yy 鼠標(biāo)198100.0

          AT 實現(xiàn)寫隔離過程的時序圖如下:

          img

          圖中,1、2、3、4 屬于第一階段,5 屬于第二階段。

          在上圖中 gtrx_1 和 gtrx_2 均成功提交,如果 gtrx_1 在第二階段執(zhí)行回滾操作,那么 gtrx_1 需要重新發(fā)起本地事務(wù)獲取本地鎖,然后根據(jù) undo_log 對這個 id=10002 的記錄進(jìn)行補(bǔ)償式回滾。此時 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ā)生臟寫的問題。整個過程的流程圖如下所示:

          img

          2)讀隔離(若有全局事務(wù)在改/寫/刪記錄,另一個全局事務(wù)對同一記錄的讀取要被隔離起來,即讀寫互斥):在數(shù)據(jù)庫本地事務(wù)的隔離級別為讀已提交、可重復(fù)讀、串行化時(讀未提交不起什么隔離作用,一般不使用),Seata AT 全局事務(wù)模型產(chǎn)生的隔離級別是讀未提交,也就是說一個全局事務(wù)會看到另一個全局事務(wù)未全局提交的數(shù)據(jù),產(chǎn)生臟讀,從前文的第一階段和第二階段的流程圖中也可以看出這一點(diǎn)。這在最終一致性的分布式事務(wù)模型中是可以接受的。

          如果要求 AT 模型一定要實現(xiàn)讀已提交的事務(wù)隔離級別,可以利用 Seata 的 SelectForUpdateExecutor 執(zhí)行器對 SELECT FOR UPDATE 語句進(jìn)行代理。SELECT FOR UPDATE 語句在執(zhí)行時會申請全局鎖,如果全局鎖已經(jīng)被其他全局事務(wù)占有,則回滾 SELECT FOR UPDATE 語句的執(zhí)行,釋放本地鎖,并且重試 SELECT FOR UPDATE 語句。在這個過程中,查詢請求會被阻塞,直到拿到全局鎖(也就是要讀取的記錄被其他全局事務(wù)提交),讀到已被全局事務(wù)提交的數(shù)據(jù)才返回。這個過程如下圖所示:

          img
          img

          4. 結(jié)束語

          XA 協(xié)議是 X/Open 提出的分布式事務(wù)處理標(biāo)準(zhǔn)。文中提到的 2PC、3PC、TCC、本地事務(wù)表、Seata in AT mode,無論哪一種,本質(zhì)都是事務(wù)協(xié)調(diào)者協(xié)調(diào)各個事務(wù)參與者的本地事務(wù)的進(jìn)度,使使所有本地事務(wù)共同提交或回滾,最終達(dá)成一種全局的 ACID 特性。在協(xié)調(diào)的過程中,協(xié)調(diào)者需要收集各個本地事務(wù)的當(dāng)前狀態(tài),并根據(jù)這些狀態(tài)發(fā)出下一階段的操作指令。這個思想就是 XA 協(xié)議的要義,我們可以說這些事務(wù)模型遵守或大致遵守了 XA 協(xié)議。

          基于消息中間件的最終一致性事務(wù)方案是互聯(lián)網(wǎng)公司在高并發(fā)場景中探索出的一種創(chuàng)新型應(yīng)用模式,利用 MQ 實現(xiàn)微服務(wù)之間的異步調(diào)用、解耦合和流量削峰,保證分布式數(shù)據(jù)記錄的最終一致性。它顯然不遵守 XA 協(xié)議。

          對于某項技術(shù),可能存在業(yè)界標(biāo)準(zhǔn)或協(xié)議,但實踐者針對具體應(yīng)用場景的需求或者出于簡便的考慮,給出與標(biāo)準(zhǔn)不完全相符的實現(xiàn),甚至完全不相符的實現(xiàn),這在工程領(lǐng)域是一種常見的現(xiàn)象。TCC 方案如此、基于消息中間件的最終一致性事務(wù)方案如此、Seata in AT mode 模式也如此。而新的標(biāo)準(zhǔn)往往就在這些創(chuàng)新中產(chǎn)生。

          你難道真的沒有發(fā)現(xiàn) 2.6 節(jié)(基于消息中間件的最終一致性事務(wù)方案)給出的正確方案中存在的業(yè)務(wù)漏洞嗎?請各位重新看下這張圖,仔細(xì)品一品兩個微服務(wù)的調(diào)用方向,把你的想法留在評論區(qū)吧 :-)

          img


          往期推薦

          我的四年大學(xué)
          歷經(jīng)兩個月,我的秋招之路結(jié)束了!
          帥地畢業(yè)了
          一份適合大眾的『學(xué)習(xí)路線』
          寫公眾號15個月以來,這一路上的學(xué)習(xí)與收獲
          那些讓你起飛的計算機(jī)基礎(chǔ)知識
          這幾年看過的優(yōu)秀基礎(chǔ)書籍介紹

          所有原創(chuàng)文章已整合成 PDF,名稱為《程序員內(nèi)功修煉》,后臺回復(fù)「PDF」即可獲取。

          瀏覽 25
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  美女裸体网站国产 | 懂色av无码任你操久久久久蜜桃av | 女学生妹毛片 | 欧美成人性生活视频 | 青青草91青娱盛宴国产 |