引言
在MySQL 5.7.7版本中,Oracle 官方將MySQL XA 一直存在的一個“bug” 進(jìn)行了修復(fù),使得MySQL XA 的實(shí)現(xiàn)符合了分布式事務(wù)的標(biāo)準(zhǔn)。那是否可以使用MySQL XA 讓MySQL 具有分布式擴(kuò)展的能力呢?在回答這個問題前,我們先看下MySQL XA 涉及到的相關(guān)概念。
相關(guān)概念介紹
事務(wù):由一個有限的數(shù)據(jù)庫操作序列構(gòu)成,這些操作需要滿足四個特性,即原子性、一致性、隔離性、持久性,簡稱ACID。
分布式事務(wù):根據(jù) Open Group 關(guān)于分布式事務(wù)的處理規(guī)范,定義了三種組件,如下圖:

其中
AP
是指應(yīng)用程序。
RM是資源管理器,事務(wù)的參與者,通常是數(shù)據(jù)庫,比如MySQL Server。一個分布式事務(wù)通常涉及多個資源管理器。
TM是事務(wù)管理器,創(chuàng)建分布式事務(wù)并協(xié)調(diào)分布式事務(wù)中的各個子事務(wù)的執(zhí)行和狀態(tài)。子事務(wù)是指分布式事務(wù)中在RM上執(zhí)行的具體操作。
兩階段提交 (Two-Phase Commit, 簡稱2PC) ,是為了使基于分布式系統(tǒng)架構(gòu)下的所有節(jié)點(diǎn)在進(jìn)行事務(wù)提交時保持一致性而設(shè)計的一種算法。分布式事務(wù)通常采用2PC,二階段提交的算法思路可以概括為: 參與者將操作成敗通知協(xié)調(diào)者,再由協(xié)調(diào)者根據(jù)所有參與者的反饋情報決定各參與者是否要提交操作還是中止操作,這里的參與者可以理解為RM,協(xié)調(diào)者可以理解為TM。如下圖所示:

在第一階段,TM會發(fā)送 Prepare 到所有參與分布式事務(wù)的RM詢問是否可以提交操作,參與分布式事務(wù)的所有RM接收到請求,實(shí)現(xiàn)自身事務(wù)提交前的準(zhǔn)備工作并返回結(jié)果。在第二階段,根據(jù)RM返回的結(jié)果,如果涉及分布式事務(wù)的所有RM都返回可以提交,則TM給RM發(fā)送commit的命令,每個RM實(shí)現(xiàn)自己的提交,同時釋放鎖和資源,然后RM反饋提交成功,TM完成整個分布式事務(wù);如果任何一個RM返回不能提交,則涉及分布式事務(wù)的所有RM都被告知需要回滾。MySQL XA 也是基于這個規(guī)范實(shí)現(xiàn)的,接下來我們介紹下MySQL XA。
MySQL XA 是什么?
MySQL XA 是基于Open Group 的<<Distributed Transaction Processing:The XA Specification>> 標(biāo)準(zhǔn)實(shí)現(xiàn)的,支持分布式事務(wù),允許多個數(shù)據(jù)庫實(shí)例參與一個全局的事務(wù)。MySQl XA 從MySQL 5.0 開始引入,僅innodb存儲引擎支持MySQL XA事務(wù)。
MySQL XA 的命令集合如下:
XA START xid: 開啟一個事務(wù),并將事務(wù)置于ACTIVE狀態(tài),此后執(zhí)行的SQL語句都將置于該是事務(wù)中。
XA END xid: 將事務(wù)置于IDLE狀態(tài),表示事務(wù)內(nèi)的SQL操作完成。
XA PREPARE xid: 實(shí)現(xiàn)事務(wù)提交的準(zhǔn)備工作,事務(wù)狀態(tài)置于PREPARED狀態(tài)。事務(wù)如果無法完成提交前的準(zhǔn)備操作,該語句會執(zhí)行失敗。
XA COMMIT xid:? 事務(wù)最終提交,完成持久化。
XA ROLLBACK xid: 事務(wù)回滾終止。
XA RECOVER: 查看MySQL中存在的PREPARED狀態(tài)的xa事務(wù)。
下圖是XA事務(wù)狀態(tài)變遷圖:

從分布式事務(wù)的變遷中可以看出,有兩條路徑可以使事務(wù)達(dá)到提交狀態(tài),有兩條路徑是回滾并結(jié)束事務(wù)。我們將這四條路徑進(jìn)行橫向?qū)Ρ?,看看每個階段是如何實(shí)現(xiàn)分布式事務(wù)的ACID特性的(相關(guān)分析是在RR隔離級別下進(jìn)行的,暫不考慮RC隔離級別)。

如上圖可以看出,
1. 當(dāng)xa start開啟事務(wù)后,DML也會在對應(yīng)的RM上創(chuàng)建undo以及read view(該read view是instance級別的)。
2. 當(dāng)xa prepare 時會將子事務(wù)置于PREPARED狀態(tài),此時子事務(wù)已經(jīng)完成事務(wù)提交前的所有準(zhǔn)備工作(獲得鎖,并將PREPARED狀態(tài)記錄到共享表空間中,會將xa start到xa end之間操作記錄在binlog中)。
3. 當(dāng)xa commit 時會在binlog中記錄xa commit xid, 并將innodb中PREPARED狀態(tài)轉(zhuǎn)化為COMMITED狀態(tài)。
4. 當(dāng)xa commit one phase 時會同時進(jìn)行prepare和commit 兩種操作,是在TM發(fā)現(xiàn)全局的分布式事務(wù)只涉及一個RM時進(jìn)行的(因?yàn)椴恍枰却渌鸕M的反饋結(jié)果)。
5. 當(dāng)xa rollback在xa prepare前時,因?yàn)闆]有寫binlog和redo,只會釋放undo, read view以及l(fā)ock。
6. 當(dāng)xa rollback 在xa prepare之后時,除了需要釋放undo, read view以及l(fā)ock,還需要binlog中記錄xa rollback xid(使得從庫不會提交該事務(wù))以及innodb中將PREPARED狀態(tài)轉(zhuǎn)化為ROLLBACK狀態(tài)。
MySQL XA 的例子
上面介紹了MySQL XA 的原理,我們現(xiàn)在舉幾個簡單的例子。
例子1,兩階段XA事務(wù)提交:
mysql> xa start 'mysql57xa';
Query OK, 0 rows affected (0.00 sec)
mysql> insert into t1(id) values(1);
Query OK, 1 row affected (0.00 sec)
mysql> xa end 'mysql57xa';
Query OK, 0 rows affected (0.00 sec)
mysql> xa prepare 'mysql57xa';
Query OK, 0 rows affected (0.00 sec)
mysql> xa recover\G
formatID: 1
gtrid_length: 7
bqual_length: 0
data: mysql57
1 row in set (0.00 sec)
mysql> xa commit 'mysql57xa';
Query OK, 0 rows affected (0.00 sec)
對應(yīng)的Binlog 中的記錄如下:

例子2,xa commit one phase ,不需要等待其他RM反饋prepare的結(jié)果。
mysql> xa start 'mysql57xa';
Query OK, 0 rows affected (0.00 sec)
mysql> insert into t1(id) values(2);
Query OK, 1 row affected (0.00 sec)
mysql> xa end 'mysql57xa';
Query OK, 0 rows affected (0.00 sec)
mysql> xa commit 'mysql57xa' one phase;
Query OK, 0 rows affected (0.00 sec)
對應(yīng)的Binlog 中的記錄如下:

例子3,在xa prepare后,執(zhí)行xa rollback 回滾事務(wù)。
mysql> xa start 'mysql57xa';
Query OK, 0 rows affected (0.00 sec)
mysql> insert into t1(id) values(3);
Query OK, 1 row affected (0.00 sec)
mysql> xa end 'mysql57xa';
Query OK, 0 rows affected (0.00 sec)
mysql> xa prepare 'mysql57xa';
Query OK, 0 rows affected (0.00 sec)
mysql> xa rollback 'mysql57xa';
Query OK, 0 rows affected (0.00 sec)
對應(yīng)的Binlog 中的記錄如下:

MySQL XA 的限制
在MySQL 5.7.7 之前,MySQL一直存在一個"bug"。在事務(wù)達(dá)到PREPARED狀態(tài)后,客戶端斷開與MySQL的連接,MySQL 會自動回滾該事務(wù),這個行為不符合分布式事務(wù)的規(guī)范,MySQL將PREPARED的事務(wù)丟失了。之所以MySQL這么實(shí)現(xiàn)是因?yàn)镸ySQL 5.7.7 之前PREPARED的事務(wù)并不會記錄到binlog中??蛻舳送顺龊髸G失該信息,如果允許再提交,那么binlog缺少事務(wù)信息,會造成主從不一致。
在MySQL 5.7.7 之后,MySQL 新增了一個XA_prepare_log_event的事件,會把xa start到xa prepare中間的操作記錄到Binlog中。Slave讀取Relay log 進(jìn)行回放,當(dāng)SQL Thread讀取到PREPARED的事務(wù)后,在讀取xa commit或者xa rollback前,會進(jìn)行一個類似客戶端斷開的操作,繼續(xù)讀取后續(xù)的事務(wù)信息,不會阻塞SQL Thread的執(zhí)行。從以上的結(jié)果看,Oracle在MySQL 5.7.7 上確實(shí)完美的解決了MySQL XA一直存在的一個"bug"。
MySQL XA 的實(shí)踐
本人曾在某公司的分布式數(shù)據(jù)庫項(xiàng)目組中實(shí)踐過基于MySQL XA的分布式事務(wù)。MySQL XA 要滿足線上高并發(fā)的訪問要求,在使用時還需要解決兩個問題:分布式死鎖問題和分布式讀一致性問題。分布式死鎖問題是指MySQL Server 是可以檢測和解決單個MySQL實(shí)例中的死鎖問題,但涉及到跨越多個MySQL 實(shí)例的分布式事務(wù)時候,需要程序?qū)用鎸?shí)現(xiàn)死鎖的檢測和解決。分布式讀一致性問題是指MySQL的read view 也是實(shí)例級別的,對于全局分布式事務(wù)來說無法實(shí)現(xiàn)讀一致,只能通過select ... lock in share mode在讀請求上加鎖的串行化隔離級別來實(shí)現(xiàn),這必然會帶來并發(fā)性能的下降。這就需要在程序?qū)用鏄?gòu)建全局的read view來實(shí)現(xiàn)全局的MVCC 。當(dāng)然這兩個問題,當(dāng)時團(tuán)隊(duì)的大牛們都已經(jīng)解決了,我也很有幸參與其中。
本人水平有限,描述有誤或是不準(zhǔn)確的地方,請大家多多指教。