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