1 數(shù)據(jù)庫事務(wù)
1.1 普通本地事務(wù)
分布式事務(wù)也是事務(wù),事務(wù)的 ACID 基本特性依舊必須符合:
A:Atomic,原子性,事務(wù)內(nèi)所有 SQL 作為原子工作單元執(zhí)行,要么全部成功,要么全部失敗;
C:Consistent,一致性,事務(wù)完成后,所有數(shù)據(jù)的狀態(tài)都是一致的。如事務(wù)內(nèi)A給B轉(zhuǎn)100,只要A減去了100,B賬戶則必定加上了100;
I:Isolation,隔離性,如果有多個事務(wù)并發(fā)執(zhí)行,每個事務(wù)作出的修改必須與其他事務(wù)隔離;
D:Duration,持久性,即事務(wù)完成后,對數(shù)據(jù)庫數(shù)據(jù)的修改被持久化存儲。
普通的非分布式事務(wù),在一個進程內(nèi)部,基于鎖依賴于快照讀和當(dāng)前讀,比較好實現(xiàn) ACID 來保證事務(wù)的可靠性。
但分布式事務(wù)參與方通常在不同機器的不同實例上,原來的局部事務(wù)的鎖不能保證分布式事務(wù)的ACID特性,需要引入新的事務(wù)框架,MySQL的分布式事務(wù)是基于2PC(二階段提交)實現(xiàn),下面詳細(xì)介紹下2pc分布式事務(wù)。
1.2 基于2pc的分布式事務(wù)
分布式事務(wù)有多種實現(xiàn)方式,如2PC(二階段提交)、3PC(三階段提交)、TCC(補償事務(wù))等,MySQL是基于 2PC 實現(xiàn)的分布式事務(wù),下面介紹 2PC 分布式事務(wù)實現(xiàn)方式。
兩階段提交:Two-Phase Commit , 簡稱2PC,為了使基于分布式系統(tǒng)架構(gòu)下的所有節(jié)點在進行事務(wù)提交時保持一致性而設(shè)計的一種算法。
2PC的算法思路可以概括為,參與者將操作成敗通知協(xié)調(diào)者,再由協(xié)調(diào)者根據(jù)所有參與者的反饋情報,決定各參與者是否要提交操作還是中止操作。這里的參與者可以理解為 Resource Manager (RM),協(xié)調(diào)者可以理解為 Transaction Manager(TM)。
下圖說明了RM和TM在分布式事務(wù)中的運作過程: 第一階段提交:TM 會發(fā)送 Prepare 到所有RM詢問是否可以提交操作,RM 接收到請求,實現(xiàn)自身事務(wù)提交前的準(zhǔn)備工作并返回結(jié)果。
第二階段提交:根據(jù)RM返回的結(jié)果,所有RM都返回可以提交,則 TM 給 RM 發(fā)送 commit 的命令,每個 RM 實現(xiàn)自己的提交,同時釋放鎖和資源,然后 RM 反饋提交成功,TM 完成整個分布式事務(wù);如果任何一個 RM 返回不能提交,則涉及分布式事務(wù)的所有 RM 都需要回滾。
2 MySQL 分布式事務(wù)XA
MySQL分布式事務(wù)XA是基于上面的2pc框架實現(xiàn),下面詳細(xì)介紹MySQL XA相關(guān)內(nèi)容。
2.1 XA事務(wù)標(biāo)準(zhǔn)
X/Open 這個組織定義的一套分布式XA事務(wù)的標(biāo)準(zhǔn),定義了規(guī)范和API接口,然后由廠商進行具體的實現(xiàn)。
XA規(guī)范中分布式事務(wù)由AP,RM,TM組成: 如上圖,應(yīng)用程序AP定義事務(wù)邊界(定義事務(wù)開始和結(jié)束),并訪問事務(wù)邊界內(nèi)的資源。資源管理器RM管理共享的資源,也就是數(shù)據(jù)庫實例。
事務(wù)管理器TM負(fù)責(zé)管理全局事務(wù),分配事務(wù)唯一標(biāo)識,監(jiān)控事務(wù)的執(zhí)行進度,并負(fù)責(zé)事務(wù)的提交、回滾、失敗恢復(fù)等。
MySQL實現(xiàn)了XA標(biāo)準(zhǔn)語法,提供了上面的RMs能力,可以讓上層應(yīng)用基于它快速支持分布式事務(wù)。
2.2 MySQL XA語法
XA START xid:開啟一個分布式事務(wù)xid。
XA END xid: 將分布式事務(wù)xid置于 IDLE 狀態(tài),表示事務(wù)內(nèi)的SQL操作完成。
XA PREPARE xid: 事務(wù)xid本地提交,成功狀態(tài)置于 PREPARED 失敗則回滾。
XA COMMIT xid: 事務(wù)最終提交,完成持久化。
XA ROLLBACK xid: 事務(wù)回滾終止。
XA RECOVER: 查看 MySQL 中存在的 PREPARED 狀態(tài)的 XA 事務(wù)。
(1)語法要點
參與分布式事務(wù)的實例之間,在數(shù)據(jù)庫內(nèi)核視角沒有直接關(guān)聯(lián),互相不感知狀態(tài),且一個分布式事務(wù)中各個節(jié)點上的子事務(wù)均可單獨執(zhí)行無依賴,他們之間的關(guān)聯(lián)是通過全局事務(wù)號在應(yīng)用層建立的。
與普通事務(wù)比,XA事務(wù)開啟時多了一個全局事務(wù)號,結(jié)束時多了一個end動作 和 prepare動作。
XA START, 開啟一個分布式事務(wù),需要指定分布式事務(wù)號。
XA END ,在內(nèi)部僅是一個狀態(tài)變化,聲明當(dāng)前XA事務(wù)結(jié)束,不允許追加新的sql語句,無其它作用,業(yè)界有人提出XA事務(wù)框架去掉這一步,減少一次網(wǎng)絡(luò)交互,提高性能。
XA PREPARE,寫 binlog 和 redo log,預(yù)提交事務(wù),并將分布式事務(wù)信息保存到全局內(nèi)存結(jié)構(gòu),讓其它連接可以查詢、回滾、提交,如果 prepare 失敗則回滾。
XA COMMIT,真正提交事務(wù),修改事務(wù)狀態(tài),釋放鎖資源。如果實例上 XA PREPARE 已經(jīng)成功,那么它的 XA COMMIT 一定能成功。
XA事務(wù)示例:201用戶給202用戶轉(zhuǎn)賬1000元,簡化如下:
第1步,開啟一個分布式事務(wù),xa_ts:10001是應(yīng)用層定義的全局事務(wù)號,實例1和實例2通過它來構(gòu)建分布式事務(wù)。
第2、3步是普通事務(wù)語句。
第4步,聲名xa事務(wù)結(jié)束,在此之后不能再追加更新插入查詢等語句,不屬于這個分布式事務(wù)也不允許,其它語句放在xa commit或xa rollback之后。
第5步,prepare 成功后,上層應(yīng)用可以發(fā)起第6步提交事務(wù)。注意,必須是所有參與這個分布式事務(wù)的全部節(jié)點均 prepare 成功,即實例1和實例2都完成prepare,應(yīng)用端才能發(fā)起提交,兩階段提交的框架核心點就在此。 如果有節(jié)點在前5步不能成功,所有參與分布式事務(wù)的節(jié)點都必須回滾。
如實例2是賬戶加1000元,基本上什么情況都能成功,肯定能成功執(zhí)行第5步,但實例1就未必了,賬戶要扣1000元,可能資金不夠,會出錯回滾,若實例1不能執(zhí)行到prepare,所有分布式事務(wù)參與者也必須回滾,所以實例2也要回滾。
如果第5步全部成功,有一個節(jié)點執(zhí)行了第6步提交了事務(wù),那么所有節(jié)點必須要均提交,否則就會導(dǎo)致數(shù)據(jù)不一致。處于xa prepare不提交會占用資源,殘留xa事務(wù)等價于存在長事務(wù),對刷臟和purge等都有影響,業(yè)務(wù)層最好要立即提交。
(2)殘留XA事務(wù)如何處理
上面說到xa事務(wù)不提交等價于長事務(wù),一旦prepare成功要立即提交,否則會帶來很多問題。
但是數(shù)據(jù)庫crash或應(yīng)用系統(tǒng)出錯crash等原因都可能導(dǎo)致xa事務(wù)未能全部提交,這些殘存XA事務(wù)如何處理?這就要用到上面的 XA RECOVER語法了,執(zhí)行xa recover 查看未提交XA事務(wù),選擇對應(yīng)的進行rollback或commit。如果僅 gtrid_length字段有值一般可以直接 xa rollback/commit xid方式回滾或提交,xid就是xa recover中data。
如果gtrid_length和bqual_length 都有值,回滾或提交則相對復(fù)雜一些,需要以下面方式提交或回滾:
gtrid 和 bqual被拼接在 data字段中,需要按他們長度切分,以下面未提交xa事務(wù)里第一個為例,gtrid_length 為34,表示data中前34個字符為gtrid, bqual_length 為22,表示data中后22個字符為bqual,那么對對其回滾或提交方式可表示如下:
如果data中有其它特殊字符,也可以轉(zhuǎn)成16進制整數(shù)方式處理,執(zhí)行語句如下:
因為是16進制數(shù),字符做了轉(zhuǎn)換,data中字符數(shù)會翻倍,回滾或提交內(nèi)容要同步調(diào)整,將data中字符也要翻倍再拆分,如上grtrid長度34,則data中前34*2個16進制數(shù)字是gtrid,bqual長度22,則后44個16進制數(shù)字是bqual,回滾或提交語法如下:
注意:上面的提交或回滾都可能報xid不存在,這不一定是xid寫錯了,也可能是開啟這個XA事務(wù)的連接并未斷開,其它連接不能處理這個XA事務(wù),這里是MySQL報錯不準(zhǔn)確。
(3)提交還是回滾的依據(jù)
上面給出如何進行提交或回滾的方法,但是提交or回滾應(yīng)該選擇哪個? 殘留XA事務(wù)是提交還是回滾,必須要由業(yè)務(wù)決定,誰開啟XA事務(wù),構(gòu)建了分布事務(wù)管理器TM,誰就必須為這個事務(wù)負(fù)責(zé)到底。
單個數(shù)據(jù)庫視角無法判斷出這個XA事務(wù)是應(yīng)該提交還是應(yīng)該回滾,不管選哪種都可能會導(dǎo)致全局?jǐn)?shù)據(jù)出錯,運維同學(xué)在處理時一定要與業(yè)務(wù)方確定好該事務(wù)是提交還是回滾,獲得授權(quán)后再操作。
以上面轉(zhuǎn)賬為例,201用戶給202轉(zhuǎn)1000元,都prepare成功,發(fā)起commit,此時202用戶實例發(fā)生故障重啟,未完成commit,重啟之后有殘留XA事務(wù),此時若201提交成功,那么202必須提交,如果201未成功,202可以先201一起提交或一起回滾,由應(yīng)用層事務(wù)管理器TM來決定。
假如201提交成功,202回滾則201扣了1000,202未收到,對賬則錢少了。如201回滾了,202提交,則202加了1000,201未扣,對賬則錢多了。
2.3 MySQL XA事務(wù)設(shè)計上的“坑”
(1)設(shè)計上的缺陷
基于binlog的主從復(fù)制是MySQL高可用的基石,這也是MySQL能廣泛流行使用的最重要因素。在MySQL內(nèi)部,對于普通事務(wù)(非XA事務(wù)),innodb等引擎和binlog為了保持?jǐn)?shù)據(jù)的一致性,就是用的 2PC ,為了區(qū)分于XA事務(wù)的2PC ,稱之為內(nèi)部兩階段提交。內(nèi)部2pc使用binlog是作為協(xié)調(diào)者(TM),內(nèi)部prepare時先寫redo再寫binlog,都持久化(受刷盤參數(shù)策略影響)后再提交。
當(dāng)發(fā)生Crash重啟時,會先恢復(fù)出所有prepare成功的事務(wù),把里面的xid事務(wù)號取出來,再到協(xié)調(diào)者Binlog中去找,如果binlog中有這個xid則說明innodb和binlog都執(zhí)行成功,等價于外部xa 事務(wù)兩個參與節(jié)點都prepare成功,則繼續(xù)提交,如果binlog中找不到,剛說明只在引擎層完成,需要回滾,如果某個進行的事務(wù)xid在prepare中未找到,則說明prepare未完成,直接回滾,這個順序一定是先寫Redo log,最后寫B(tài)inlog。
那么處于XA prepare 狀態(tài)的分布式事務(wù)到底是一個什么樣的狀態(tài)?分布式XA事務(wù)也是基于普通事務(wù)實現(xiàn),實際上就是一個支持掛起,支持讓其它會話繼續(xù)提交或回滾,支持crash或重啟之后還能恢復(fù)這種掛起狀態(tài)的普通事務(wù)。 普通事務(wù)的prepare動作是發(fā)生在顯式commit之后,先寫redo后再寫binlog。
XA事務(wù)的prepare發(fā)生在顯式XA commit之前,它需要生成binlog,然后再寫redo,這與普通事務(wù)是相反的,這就導(dǎo)致這個外部2pc事務(wù)的內(nèi)部2pc提交缺少了一個協(xié)調(diào)者,某些情況下會導(dǎo)致數(shù)據(jù)庫不一致。
一個XA事務(wù)的binlog由兩部分組成,從xa start到xa prepare是一個不可分原子語句塊,xa commit又是一個原子語句塊,且分別有各自的gtid,如下圖binlog:
?
事務(wù)號為 X'7831',X'',1 的分布式事務(wù)prepare之后,中間插入了很多普通事務(wù),然后再執(zhí)行的xa commit。 一個XA事務(wù)的binlog被切分成了兩個獨立的部分,如果在主節(jié)點在生成XA prepare binlog之后發(fā)生crash, 還沒有在引擎層做prepare,重啟之后引擎層中因沒有完成prepare動作而回滾。
但在主從架構(gòu)中,只要binlog正常產(chǎn)生就可能會同步到Slave機,這種情況下會導(dǎo)致slave機上多了這個xa prepare的中間狀事務(wù),最終復(fù)制出現(xiàn)問題。這個問題已經(jīng)被發(fā)現(xiàn)多年,官方確認(rèn)了bug,一直未修復(fù)。
(2)遇到該問題處理思路
雖然我們要盡量避免出現(xiàn)故障,但也做好面對任何故障的準(zhǔn)備,謀而后動,有招不亂! 在常規(guī)連接中,MySQL的XA事務(wù)執(zhí)行prepare之后,通常不能執(zhí)行其它非xa語句,會報錯提醒當(dāng)前正在xa事務(wù)中。
但在復(fù)制的sql 回放線程中,執(zhí)行完xa prepare之后,可以直接執(zhí)行其它非此xa事務(wù)的sql,因為在master端生成的XA事務(wù)Binlog可能就是分開的,如上圖例子就是。
所以slave機sql線程執(zhí)行完xa prepare的binlog后,是被允許接著正常執(zhí)行其它事務(wù)的binlog的。如果xa preapre過程master上發(fā)生crash,剛好生成了binlog,但沒有做完后續(xù)的prepare動作,備機收到了這個xa preare動作的binlog,master重啟后會回滾掉這個事務(wù),不會再生成這個xa事務(wù)后續(xù)binlog,這會導(dǎo)致備機執(zhí)行完xa prepare后一直掛起,占用的鎖等資源不會釋放,直到新同步過來的binlog與之沖突報錯,才會暴露問題。
要修復(fù)分兩種情況處理:
情況1:基于gtid的復(fù)制,應(yīng)該直接會報gtid重復(fù)錯誤(推測,本地沒能復(fù)現(xiàn))。master上重啟應(yīng)該會回滾掉了前半個XA事務(wù),后面事務(wù)會重新生成這個相同gtid的事務(wù),導(dǎo)致復(fù)制出錯,此時停止復(fù)制,將備機上這半個XA事務(wù)回滾,并reset gtid到之前的gtid,重建復(fù)制即可。注意這里可能有多個XA事務(wù)在Binlog中處于prepare狀態(tài),需要解析binlog仔細(xì)確定要回滾的事務(wù)是哪個。
情況2:未開gtid的復(fù)制,此時比上面情況要麻煩,沒有g(shù)tid來確定binlog事務(wù)是否重復(fù),只要后面事務(wù)不涉及到這半個xa事務(wù)鎖定的資源,備機就可以正常維持復(fù)制體系,一直同步數(shù)據(jù),等到有沖突數(shù)據(jù)出現(xiàn)錯誤,回放線程重試超過一定次數(shù)后(slave_transaction_retries重試參數(shù)控制),sql線程報出相應(yīng)錯誤,復(fù)制中斷后才能被感知。
恢復(fù)數(shù)據(jù)和上面差不多,回滾這個XA事務(wù),重建主從,但是這個事務(wù)的binlog不一定能找到,因為沒有g(shù)tid不會立即報錯,可能幾分鐘后報錯,也可能幾個月后報錯,取決于業(yè)務(wù)什么時候產(chǎn)生沖突數(shù)據(jù)。并且在這個事務(wù)之后,從機又同步了很多數(shù)據(jù),這些數(shù)據(jù)是否可靠需要評估。線上強烈建議開啟Gtid復(fù)制模式,非gtid的復(fù)制官方已經(jīng)在淘汰!
3 分布式事務(wù)的一致性
使用到分布式事務(wù),就必須要保證分布式事務(wù)的一致性。 分布式事務(wù)的一致性又分寫一致性和讀一致性,寫一致性XA框架XA prepare 和XA commit已經(jīng)解決,只要保證有提交全提交,有回滾全回滾就能保證寫一致性。 讀一致性則要復(fù)雜的多,先看看MySQL官方對XA事務(wù)在讀一致性上的“只言片語”:
上面內(nèi)容是從官方說明文檔里截取,里面對XA讀一致性略有介紹:如果應(yīng)用程序?qū)ψx敏感,首選SERIALIZABLE隔離級別,RR級別不足以用于分布式事務(wù),官方?jīng)]有對這里的不足做具體說明,但我們可以構(gòu)建一個例子來分析這個“may not be sufficien”來描述讀一致性是否恰當(dāng)。
如下圖,有A、B兩個賬戶在兩個實例上,假設(shè)每個賬戶初始都100塊,A給B轉(zhuǎn)賬20,時間線左邊為A賬戶實例上的操作,右邊為B賬戶實例上的操作,中間T1到T6為不同時間點。
T1時刻:初始均100。 T2時刻:AB賬戶均完成xa prepare操作,一個減20,一個加20。 T3時刻:A帳戶節(jié)點XA commit成功。
T5時刻:B帳戶XA commit成功。
當(dāng)處在RR或RC隔離級別時,發(fā)起一個對賬操作,統(tǒng)計AB帳戶資金總額,當(dāng)只有他們相互轉(zhuǎn)賬時,總金額應(yīng)該恒為200。
T6 時刻時,查詢A為80,B為120,總賬為200,無問題。
T4時刻查詢A賬戶為80,查詢B賬戶時由于MVCC機制,會讀到上個快照中的值100,加一起為180,總賬不對。
因為是操作不同實例,當(dāng)開始做xa commit之后,可能由于網(wǎng)絡(luò)等原因,并不能保證所有節(jié)點的XA commit同時到達所有節(jié)點,在一個高并發(fā)場景,導(dǎo)致上面的問題幾乎是必然的。
因此,當(dāng)使用MySQL 原生XA分布式事務(wù)時,若無其它手段來保障讀一致性,而應(yīng)用又有跨節(jié)點讀的應(yīng)用場景,應(yīng)當(dāng)使用序列化(SERIALIZABLE)隔離級別,“may not be sufficien”顯然是不恰當(dāng)?shù)?,沒有任何一個業(yè)務(wù)能接受這種數(shù)據(jù)統(tǒng)計不對的。
如果是序列化隔離級別,T4時刻讀到A為80,讀B時會等待,直到T5時刻XA commit成功之后, 才能讀到B為120,總賬200,無問題。
序列化隔離級別只有讀-讀不阻塞,讀-寫,寫-讀,寫-寫均會阻塞,而RC、RR僅寫-寫阻塞,因此只有序列化隔離級才能充分保障MySQL XA事務(wù)的讀一致性。
但它阻塞太多,性能也是各種隔離級別中最差的,所以如無必要,通常不會使用這一隔離級別。
業(yè)界有很多方案來解決分布式事務(wù)RR、RC下的讀一致性問題,以提高數(shù)據(jù)庫性能,但原生的MySQL不具備這種能力,因此使用MySQL原生XA事務(wù)的業(yè)務(wù)需要謹(jǐn)慎選擇隔離級別。
4 小結(jié)
只要我們小心面對殘留XA事務(wù),謹(jǐn)慎處理Crash之后的可能存在的多余binlog數(shù)據(jù),認(rèn)真評估使用RR、RC隔離級別是否有讀一致性讀問題等問題之后,MySQL 的XA事務(wù)基本沒有其它問題,可以作為RM完備提供跨節(jié)點分布式事務(wù)能力,MySQL已經(jīng)實現(xiàn)了X/Open 組織定義的分布式事務(wù)處理規(guī)范中的語法功能,完全可以放心放業(yè)務(wù)在這條路上奔跑!
審核編輯:劉清
-
管理器
+關(guān)注
關(guān)注
0文章
246瀏覽量
18545 -
MYSQL數(shù)據(jù)庫
+關(guān)注
關(guān)注
0文章
96瀏覽量
9412
原文標(biāo)題:MySQL 分布式事務(wù)的“路”與“坑”
文章出處:【微信號:AI前線,微信公眾號:AI前線】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論