MySQL分布式事務(wù)介紹
InnoDB存儲引擎提供了對XA事務(wù)的支持,并通過XA事務(wù)來支持分布式事務(wù)的實現(xiàn)。分布式事務(wù)指的是允許多個獨立的事務(wù)資源參與到一個全局的事務(wù)中。事務(wù)資源通常是關(guān)系型數(shù)據(jù)庫系統(tǒng),但也可以是其他類型的資源。全局事務(wù)要求在其中的所有參與的事務(wù)要么都提交,要么都回滾,這對于事務(wù)原有的ACID要求又有了提高。另外,在使用分布式事務(wù)時,InnoDB存儲引擎的事務(wù)隔離級別必須設(shè)置為SERIALIZABLE。
XA事務(wù)語允許不同數(shù)據(jù)庫之間的分布式事務(wù),如一臺服務(wù)器是MySQL數(shù)據(jù)庫的,另一臺是Oracle數(shù)據(jù)庫的,又可能還有一臺服務(wù)器是SQL Server數(shù)據(jù)庫的,只要參與在全局事務(wù)中的每個節(jié)點都支持XA事務(wù)。分布式事務(wù)可能在分布式架構(gòu)或銀行系統(tǒng)的轉(zhuǎn)賬中比較常見,入用戶David需要從上海轉(zhuǎn)10000元到北京的用戶Mariah的銀行卡中。
# Bank@Shanghai
update account set money = money - 10000 where user='David';
# Bank@Beijing
update account set money = money + 10000 where user='Mariah';
在這種情況下,一定需要使用分布式事務(wù)來保證數(shù)據(jù)的安全。如果發(fā)生的操作不能全部提交或回滾,那么任何一個節(jié)點出現(xiàn)問題都會導(dǎo)致嚴(yán)重的結(jié)果。要么是David的賬號被扣了款,但Mariah沒收到,又或者是David的賬戶沒有扣款,Mariah卻收到錢了。
XA事務(wù)由一個或多個資源管理器(resource managers)、一個事務(wù)管理器(transaction manager)以及一個應(yīng)用程序(application program)組成。
資源管理器:提供訪問事務(wù)資源的方法,通常一個數(shù)據(jù)庫就是一個資源管理器。
事務(wù)管理器:協(xié)調(diào)參與全局事務(wù)中的各個事務(wù),需要和參與全局事務(wù)的所有資源管理器進(jìn)行通信。
應(yīng)用程序:定義事務(wù)的邊界,指定全局事務(wù)中的操作。
在MySQL數(shù)據(jù)庫的分布式事務(wù)中,資源管理器就是MySQL數(shù)據(jù)庫,事務(wù)管理器為連接MySQL服務(wù)器的客戶端。下圖顯示了一個分布式事務(wù)的模型。

分布式事務(wù)通常采用2PC協(xié)議,全稱Two Phase Commitment Protocol。該協(xié)議主要為了解決在分布式數(shù)據(jù)庫場景下,所有節(jié)點間數(shù)據(jù)一致性的問題。在分布式事務(wù)環(huán)境下,事務(wù)的提交會變得相對比較復(fù)雜,因為多個節(jié)點的存在,可能存在部分節(jié)點提交失敗的情況,即事務(wù)的ACID特性需要在各個數(shù)據(jù)庫實例中保證??偠灾?,在分布式提交時,只要發(fā)生一個節(jié)點提交失敗,則所有的節(jié)點都不能提交,只有當(dāng)所有節(jié)點都能提交時,整個分布式事務(wù)才允許被提交。
分布式事務(wù)通過2PC協(xié)議將提交分成兩個階段,在第一階段,所有參與全局事務(wù)的節(jié)點都開始準(zhǔn)備(PREPARE),告訴事務(wù)管理器它們準(zhǔn)備好提交了。在第二階段,事務(wù)管理器告訴資源管理器執(zhí)行ROLLBACK還是COMMIT。如果任何一個節(jié)點顯示不能提交,則所有的節(jié)點都被告知需要回滾??梢娕c本地事務(wù)不同的是,分布式事務(wù)需要多一次的PREPARE操作,待收到所有節(jié)點的同意信息后,再進(jìn)行COMMIT或是ROLLBACK操作。
MySQL分布式事務(wù)操作
1. MySQL XA事務(wù)的語法
主要有:
# 在mysql實例中開啟一個XA事務(wù),指定一個全局唯一標(biāo)識;
mysql> XA START 'any_unique_id';
# XA事務(wù)的操作結(jié)束;
mysql> XA END 'any_unique_id?';
# 告知mysql準(zhǔn)備提交這個xa事務(wù);
mysql> XA PREPARE 'any_unique_id';
# 告知mysql提交這個xa事務(wù);
mysql> XA COMMIT 'any_unique_id';
# 告知mysql回滾這個xa事務(wù);
mysql> XA ROLLBACK 'any_unique_id';
# 查看本機(jī)mysql目前有哪些xa事務(wù)處于prepare狀態(tài);
mysql> XA RECOVER;
2. XA事務(wù)恢復(fù)
如果執(zhí)行分布式事務(wù)的mysql crash了,MySQL按照如下邏輯進(jìn)行恢復(fù):
a. 如果這個xa事務(wù)commit了,那么什么也不用做。
b. 如果這個xa事務(wù)還沒有prepare,那么直接回滾它。
c. 如果這個xa事務(wù)prepare了,還沒commit, 那么把它恢復(fù)到prepare的狀態(tài),由用戶去決定commit或rollback。
當(dāng)mysql crash后重新啟動之后,執(zhí)行“XA RECOVER;”查看當(dāng)前處于prepare狀態(tài)的xa事務(wù),然后commit或rollback它們。
MySQL分布式事務(wù)限制
a. XA事務(wù)和本地事務(wù)以及鎖表操作是互斥的
開啟了xa事務(wù)就無法使用本地事務(wù)和鎖表操作
mysql> xa start 't1xa';
Query OK, 0 rows affected (0.04 sec)
mysql> begin;
ERROR 1399 (XAE07): XAER_RMFAIL: The command cannot be executed when global transaction is in theACTIVE state
mysql> lock table t read;
ERROR 1399 (XAE07): XAER_RMFAIL: The command cannot be executed when global transaction is in theACTIVE state
開啟了本地事務(wù)就無法使用xa事務(wù)
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> xa start 'rrrr';
ERROR 1400 (XAE09): XAER_OUTSIDE: Some work is done outside global transaction
b. xa start之后必須xa end,否則不能執(zhí)行xa commit和xa rollback
所以如果在執(zhí)行xa事務(wù)過程中有語句出錯了,你也需要先xa end一下,然后才能xa rollback。
mysql> xa start 'tt';
Query OK, 0 rows affected (0.00 sec)
mysql> xa rollback 'tt';
ERROR 1399 (XAE07): XAER_RMFAIL: The command cannot be executed when global transaction is in the??ACTIVE state
mysql> xa end 'tt';
Query OK, 0 rows affected (0.00 sec)
mysql> xa rollback 'tt';
Query OK, 0 rows affected (0.00 sec)
MySQL 5.7對分布式事務(wù)的支持
一直以來,MySQL數(shù)據(jù)庫是支持分布式事務(wù)的,但是只能說是有限的支持,具體表現(xiàn)在:
已經(jīng)prepare的事務(wù),在客戶端退出或者服務(wù)宕機(jī)的時候,2PC的事務(wù)會被回滾。
在服務(wù)器故障重啟提交后,相應(yīng)的Binlog被丟失。
上述問題存在于MySQL數(shù)據(jù)庫長達(dá)數(shù)十年的時間,直到MySQL-5.7.7版本,官方才修復(fù)了該問題。下面將會詳細(xì)介紹下該問題的具體表現(xiàn)和官方修復(fù)方法,這里分別采用官方MySQL-5.6.27版本(未修復(fù))和MySQL-5.7.9版本(已修復(fù))進(jìn)行驗證。
先來看下存在的問題,我們先創(chuàng)建一個表如下:
CREATE TABLE t(
id INT AUTO_INCREMENT PRIMARY KEY,
a INT
)ENGINE=InnoDB;
對于上述表,通過如下操作進(jìn)行數(shù)據(jù)插入:
mysql> XA START 'mysql56';
mysql> INSERT INTO t VALUES(1,1);
mysql> XA END 'mysql56';
mysql> XA PREPARE 'mysql56';
通過上面的操作,用戶創(chuàng)建了一個分布式事務(wù),并且prepare沒有返回錯誤,說明該分布式事務(wù)可以被提交。通過命令XA?RECOVER查看顯示如下結(jié)果:
mysql> XA RECOVER;
+----------+--------------+--------------+---------+
| formatID | gtrid_length | bqual_length | data? |
+----------+--------------+--------------+---------+
| 1? ? | 7? ? ? | 0? ? ? | mysql56 |
+----------+--------------+--------------+---------+
若這時候用戶退出客戶端后重連,通過命令xa recover會發(fā)現(xiàn)剛才創(chuàng)建的2PC事務(wù)不見了。即prepare成功的事務(wù)丟失了,不符合2PC協(xié)議規(guī)范?。。?/i>
產(chǎn)生上述問題的主要原因在于:MySQL 5.6版本在客戶端退出的時候,自動把已經(jīng)prepare的事務(wù)回滾了,那么MySQL為什么要這樣做?這主要取決于MySQL的內(nèi)部實現(xiàn),MySQL 5.7以前的版本,對于prepare的事務(wù),MySQL是不會記錄binlog的(官方說是減少fsync,起到了優(yōu)化的作用)。只有當(dāng)分布式事務(wù)提交的時候才會把前面的操作寫入binlog信息,所以對于binlog來說,分布式事務(wù)與普通的事務(wù)沒有區(qū)別,而prepare以前的操作信息都保存在連接的IO_CACHE中,如果這個時候客戶端退出了,以前的binlog信息都會被丟失,再次重連后允許提交的話,會造成Binlog丟失,從而造成主從數(shù)據(jù)的不一致,所以官方在客戶端退出的時候直接把已經(jīng)prepare的事務(wù)都回滾了!
官方的做法,貌似干得很漂亮,犧牲了一點標(biāo)準(zhǔn)化的東西,至少保證了主從數(shù)據(jù)的一致性。但其實不然,若用戶已經(jīng)prepare后在客戶端退出之前,MySQL發(fā)生了宕機(jī),這個時候又會怎樣?
MySQL在某個分布式事務(wù)prepare成功后宕機(jī),宕機(jī)前操作該事務(wù)的連接并沒有斷開,這個時候已經(jīng)prepare的事務(wù)并不會被回滾,所以在MySQL重新啟動后,引擎層通過recover機(jī)制能恢復(fù)該事務(wù)。當(dāng)然該事務(wù)的Binlog已經(jīng)在宕機(jī)過程中被丟失,這個時候,如果去提交,則會造成主從數(shù)據(jù)的不一致,即提交沒有記錄Binlog,從上丟失該條數(shù)據(jù)。所以對于這種情況,官方一般建議直接回滾已經(jīng)prepare的事務(wù)。
以上是MySQL 5.7以前版本MySQL在分布式事務(wù)上的各種問題,那么MySQL 5.7版本官方做了哪些改進(jìn)?這個可以從官方的WL#6860描述上得到一些信息,我們還是本著沒有實踐就沒有發(fā)言權(quán)的態(tài)度,從具體的操作上來分析下MySQL 5.7的改進(jìn)方法。還是以上面同樣的表結(jié)構(gòu)進(jìn)行同樣的操作如下:
mysql> XA START 'mysql57';
mysql> INSERT INTO t VALUES(1,1);
mysql> XA END 'mysql57';
mysql> XA PREPARE 'mysql57'
這個時候,我們通過mysqlbinlog來查看下Master上的Binlog,結(jié)果如下:

同時也對比下Slave上的Relay log,如下:

通過上面的操作,明顯發(fā)現(xiàn)在prepare以后,從XA START到XA PREPARE之間的操作都被記錄到了Master的Binlog中,然后通過復(fù)制關(guān)系傳到了Slave上。也就是說MySQL 5.7開始,MySQL對于分布式事務(wù),在prepare的時候就完成了寫B(tài)inlog的操作,通過新增一種叫XA_prepare_log_event的event類型來實現(xiàn),這是與以前版本的主要區(qū)別(以前版本prepare時不寫B(tài)inlog)。
當(dāng)然僅靠這一點是不夠的,因為我們知道Slave通過SQL thread來回放Relay log信息,由于prepare的事務(wù)能阻塞整個session,而回放的SQL thread只有一個(不考慮并行回放),那么SQL thread會不會因為被分布式事務(wù)的prepare階段所阻塞,從而造成整個SQL thread回放出現(xiàn)問題?這也正是官方要解決的第二個問題:怎么樣能使SQL thread在回放到分布式事務(wù)的prepare階段時,不阻塞后面event的回放?其實這個實現(xiàn)也很簡單(在xa.cc::applier_reset_xa_trans),只要在SQL thread回放到prepare的時候,進(jìn)行類似于客戶端斷開連接的處理即可(把相關(guān)cache與SQL thread的連接句柄脫離)。最后在Slave服務(wù)器上,用戶通過命令XA RECOVER可以查到如下信息:
mysql> XA RECOVER;
+----------+--------------+--------------+---------+
| formatID | gtrid_length | bqual_length | data? |
+----------+--------------+--------------+---------+
| 1? ? | 7? ? ? | 0? ? ? | mysql57 |
+----------+--------------+--------------+---------+
至于上面的事務(wù)什么時候提交,一般等到Master上進(jìn)行XA COMMIT ?‘mysql57’后,slave上也同時會被提交。
總結(jié)
綜上所述,MySQL 5.7對于分布式事務(wù)的支持變得完美了,一個長達(dá)數(shù)十年的bug又被修復(fù)了,因而又多了一個升級到MySQL 5.7版本的理由。