關於XA,分佈式事務處理的原理,可見[3];關於MySQL XA的說明,可見[1][2]。html
MySQL XA分爲兩類,內部XA與外部XA;內部XA用於同一實例下跨多個引擎的事務,由你們熟悉的Binlog做爲協調者;外部XA用於跨多MySQL實例的分佈式事務,須要應用層介入做爲協調者(崩潰時的懸掛事務,全局提交仍是回滾,須要由應用層決定,對應用層的實現要求較高);mysql
本文,假設讀者已經知道MySQL外部XA的使用,而將重點放在MySQL如何處理外部XA的crash recover,以及面對不一樣的crash recover的情形,應用程序如何處理,纔可以保證分佈式事務的一致性。最後,本文簡單分析一下目前MySQL外部XA支持存在的問題,以及可選的解決方案。sql
源代碼分析基於MySQL 5.1.49,MySQL 5.5.16。數據庫
MySQL外部XA的正常處理流程,這裏不許備介紹,能夠參考[1][2][3]。接下來我重點描述一下MySQL外部XA的崩潰恢復流程,畢竟此流程跟應用程序如何正確使用外部XA息息相關。緩存
若一個運行外部XA事務的MySQL節點發生崩潰,那麼其重啓以後的崩潰恢復,涉及到外部XA處理的流程以下:安全
Crash recover:服務器
// 1. 讀取binlog文件,將文件中的xid存入commit_list hash表網絡
// 顧名思義,所謂的commit_list,就是說此list中對應prepare狀態的xidjsp
// 在崩潰恢復過程當中都可以被提交,而不在commit_list中的xid,均須回滾分佈式
// binlog中的xid,都是屬於內部xid,由MySQL產生,用於內部XA
Log.cc::TC_LOG_BINLOG::recover
// 2. 遍歷底層全部的事務引擎,收集處於XA_PREPARED狀態的全部xid
// 這些xid列表,既包括內部xid,也包括外部xid,存儲引擎內部不作區分
Handler.cc::ha_recover(commit_list)
// 執行各引擎層面提供的recover方法,收集全部的處於prepared狀態的xid
// 根據xid分類:
// 3. 若xid屬於內部xid,那麼在commit_list中查找此xid,
// 若存在,則提交此xid對應的事務;不然,回滾此事務
// 4. 若xid屬於外部xid,那麼則將xid插入xid_cache hash表
// xid_cache中的全部xid,將會經過xa recover命令返回,等待外部程序決策
Handler.cc::xarecover_handlerton
// 5. 收集InnoDB引擎中,處於prepare狀態的全部xid,並返回
got = hton->recover(innobase_xa_recover)
my_xid x = info->list[i].get_my_xid();
if (!x)
// 若當前爲外部xid,那麼將xid插入xid_cache hash表
xid_cache_insert(&xid_cache, x);
else
if (x in commit_list)
// 若當前爲內部xid,同時此xid在binlog中存在,則提交
hton->commit_by_xid();
else
// 若當前爲內部xid,同時此xid在binlog中不存在,則回滾
hton->rollback_by_xid();
經過以上的分析,能夠總結出:
xa recover命令處理流程:
sql_parse.cc::mysql_execute_command
case SQLCOM_XA_RECOVER:
mysql_xa_recover();
// 遍歷xid_cache,找出其中的狀態處於XA_PREPARED的事務,發送客戶端
while (xs = hash_element(&xid_cache,))
if (xs->xa_state == XA_PREPARED)
protocol->write();
根據xa recover命令收集到的各MySQL實例返回的xid列表,而後再對比應用程序端日誌,決定這些xid,哪些全局commit,哪些rollback。
因爲測試中只有一個MySQL實例,所以此時能夠直接選擇commit處於prepare狀態的xid。
上面提到,MySQL有外部XA與內部XA,內部XA對應的xid由MySQL內部產生,有特定的格式:
MYSQL_XID_PREFIX: MySQLXid(源碼寫死) 8 bytes
server_id: MySQL實例的id,ulong, 4 bytes
my_xid: 內部自增序列,ulonglong, 8 bytes
MySQL內部xid由以上3部分組成,總長度爲20。
判斷是否爲內部xid的代碼以下:
gtrid_length == MYSQL_XID_GTRID_LEN
&&bqual_length == 0
&&!memcmp(data, MYSQL_XID_PREFIX, MYSQL_XID_PREFIX_LEN)
其中:MYSQL_XID_GTRID_LEN = 20;MYSQL_XID_PREFIX_LEN = 8;
例如:「MySQLXid 0004「
server_id = 」;my_xid = 4
所以,使用時應該注意,不要在外部構造這種形式的xid,不然MySQL就會將內部xid與外部xid混淆。
在測試中,我構造了一個外部xid = ‘MySQLXidxxxx00100000′,長度爲20 bytes,前八個字符爲‘MySQLXid’。在事務完成xa prepare以後,關閉MySQL數據庫。MySQL在crash recover時,直接將此xid認爲是內部xid,並在內部由Binlog直接rollback此事務,致使使用xa recover命令沒法看到任何prepare狀態的xa事務。
可是,反過來考慮,如果應用程序自己不想處理懸掛事務,那麼將外部xid構形成內部的形式不失爲一種較好的策略,由binlog來負責處理懸掛事務的提交與回滾。付出的代價則是:崩潰時,未提交事務在各個MySQL實例上的狀態可能不一致(部分節點提交;部分節點回滾)。
前面提到了MySQL外部XA的崩潰恢復流程。在本小節咱們簡單分析一下崩潰恢復過程當中的Binlog文件的讀取問題。
經過跟蹤TC_LOG_BINLOG::open函數,發如今crash recover過程當中,MySQL全量讀取最後一個Binlog文件,這與MariaDB WorkLog#164:Extend crash recovery to recover non-prepared transactions from binlog[6]中的說法一致:...The existing scan always scans the full last binlog file, and we should keep this...
可是這樣就帶來一個疑問:
爲何僅僅全量讀取最後一個Binlog文件就能夠呢?若是最後一個binlog文件很短,如何保證底層引擎處於prepare狀態的事務不會出如今前一個Binlog文件之中?
回答這個疑問,須要從目前MySQL寫Binlog與底層存儲引擎(InnoDB)寫redo log的方式分析:
上面說到,因爲MySQL+InnoDB不支持group commit,所以只讀最後一個Binlog是可行的,那麼若是是最新版的Percona/MariaDB,已經支持group commit (關於group commit的具體實現,能夠參考個人另一篇短文:MariaDB&PerconaXtraDB Group Commit實現簡要分析[7]),那麼仍舊讀取最後一個Binlog文件是否同樣可行呢?
答案是確定的,由於目前Percona/MariaDB的最新版本實現中,仍舊採用的是全量讀取最後一個Binlog文件的策略,那麼此時又是如何保證前一個Binlog文件中全部的日誌對應的事務,其在底層InnoDB引擎中已經完成提交動做了呢?
通過閱讀MariaDB 5.3.4的代碼,我找到了答案:
一樣仍是在MariaDB WL#164[6]中,提到了遍歷binlog的一種優化,目前,InnoDB redo log在commit日誌中已經記錄了對應的Binlog日誌的(文件名,位置)信息。只要將此信息返回,就能夠從指定位置開始遍歷Binlog,如此一來,使用更大的Binlog文件,也不會影響crash recovery時,讀取Binlog文件的性能。
MySQL外部XA能夠用在分佈式數據庫代理層,例如開源的代理工具:ameoba[4],網易的DDB,淘寶的TDDL,B2B的Cobar等等。
經過MySQL外部XA,這些工具能夠提供跨庫的分佈式事務。固然,這些工具也就成了外部XA事務的協調者角色。在crash recover時控制懸掛事務是全局commit,或者rollback。
在crash recover以後,外部應用程序可能會遇到如下幾種狀況:
經過前面的分析,可知應用程序配合MySQL的XA事務功能,可以較好的支持分佈式環境下的事務。可是,這個支持並不完美,根據個人分析,有可能會出現如下幾個問題:
MySQL的主備庫的同步,經過Binlog的複製完成。而Binlog是MySQL內部XA事務的協調者,而且MySQL爲binlog作了優化——binlog不寫prepare日誌,只寫commit日誌。
考慮前面提到的狀況二,全部的參與節點prepare完成,在進行xa commit前crash。crash recover若是選擇commit此事務。因爲binlog在prepare階段未寫,所以主庫中看來,此分佈式事務最終提交了,可是此事務的操做並未寫到binlog中,所以也就未能成功複製到備庫,從而致使主備庫數據不一致的狀況出現。
在MySQL 5.5.16版本中作過測試,這個問題實際存在。crash recover以後,對xa recover返回的事務運行xa commit,對應事務提交,可是操做並未寫入binlog,所以沒法複製到備庫。
那麼是否回滾全部prepare的事務,就能夠避免此問題呢?結論是仍舊不行,不只不能解決問題一,甚至可能引發問題二。
若回滾全部prepare狀態的分佈式事務,會產生問題二。考慮狀況三(全部節點完成prepare,部分節點完成commit),該分佈式事務對應的節點,部分已經提交,沒法回滾,而部分節點回滾。最終致使同一分佈式事務,在各參與節點,最終狀態不一致。
在MySQL 5.1.49中,全部xa recover返回的外部xid,都不能被提交。緣由以下:
當運行xa commit ‘xid_name’命令時,MySQL會判斷當前xid_name的錯誤信息,若存在錯誤信息,那麼就在內部將xa commit命令強制轉換爲xa rollback。xid_name的狀態存於xid_cache中,在crash recover階段,由函數Handler.cc::xarecover_handlerton調用xid_cache_insert(&xid_cache, x)函數完成插入。MySQL 5.1.49在實現xid_cache_insert函數有bug。
…
xs->xa_state=xa_state;
xs->xid.set(xid);
xs->in_thd=0;
xs->rm_error=0;
res=my_hash_insert(&xid_cache, (uchar*)xs);
…
MySQL 5.1.49中,缺乏了xs->rm_error =0這一行,未初始化rm_error,致使xa commit時判斷出錯,沒法commit。MySQL 5.5.16已經fix此bug,加上了黑色這一行的初始化,應用程序能夠xa commit。
從MySQL外部XA不足的分析能夠看出,除了實現bug以外,產生其他兩個問題的最大緣由,仍是在於MySQL針對binlog作的」middle engine」優化,binlog的prepare不寫日誌。在MySQL內部XA事務中,這個優化是可行的,由於Binlog自己的角色就是事務協調者(Transaction Coordinator),事務協調者能夠不進行prepare [5]。
可是對於MySQL外部XA事務,Binlog已經不是事務協調者的角色,其也是一個參與者,或者說是Resource Manager。所以Binlog的prepare日誌是不可省略的。
爲了解決MySQL外部XA事務crash recover過程當中出現的問題,我以爲只能修改binlog模塊。使binlog模塊在正常運行過程當中也區份內部XA事務與外部XA事務。內部XA事務能夠仍舊沿用如今的方案;而外部XA事務,須要增長寫prepare日誌的功能,已經crash recover時處理prepare日誌的功能。
[1] Sergei Golubchik.Distributed Transaction Processing with MySQL XA
[2] http://dev.mysql.com/doc/refman/5.1/en/xa.html
[3] X/Open.Distributed TP: The XA Specification
[4] 陳思儒. Amoeba
[5] MariaDB WorkLog#132: Transaction coordinator plugin
[6] MariaDB WorkLog#164: Extend crash recovery to recover non-prepared transactions from binlog
[7] 何登成. MariaDB&PerconaXtraDB Group Commit實現簡要分析
轉載來自:http://hedengcheng.com/?p=136
《高性能MySQL(第二版)》關於分佈式XA事務的說明
5.11 分佈式(XA)事務
Distributed(XA) Transactions
存儲引擎事務在存儲引擎內部被賦予了ACID(譯註1)屬性,分佈式(XA)事務是一種高層次事務,它能夠利用兩段提交的方式將ACID屬性擴展到存儲引擎外部,甚至數據庫外部。MySQL 5.0及其以上的版本部分支持XA事務。
XA事務須要事務協調員,它會通知全部的參與者準備提交事務(階段一)。當協調員從全部參與者那裏收到"就緒(Ready)"信號時,它會通知全部參與者進行真正的提交(階段二)。MySQL能夠是XA事務的參與者,但不能是協調員。
MySQL內部其實有兩種XA事務。MySQL服務器能參與由外部管理的分佈式事務,但它內部使用了XA事務來協調存儲引擎和二進制日誌。
5.11.1 內部XA事務
Internal XA Transactions
MySQL內部使用XA事務的緣由是服務器和存儲引擎之間是隔離的。存儲引擎之間是徹底獨立的,彼此不知道對方的存在,因此任何跨引擎的事務本質上都是分佈的,而且要求第三方來進行協調。MySQL就是第三方。假如沒有XA事務,跨引擎事務提交須要順序地要求每一個引擎進行提交。這樣就會引入一種可能,就是在某個引擎提交以後發生了崩潰,可是另一個引擎還未提交。這就打破了事務的原則。
若是把記錄事件的二進制日誌當作一個"存儲引擎",那麼就能理解爲何即便是單個事務性引擎也須要XA事務。存儲引擎把事件提交給二進制日誌時,須要和服務器進行同步,由於是服務器,而不是存儲引擎處理二進制日誌。
當前的XA在性能上有些進退兩難。它打破了InnoDB從MySQL 5.0以來的對羣體提交(Group Commit)(一種使用單次I/O提交多個事務的技術)的支持,因此會致使了不少fsync()調用。若是二進制日誌處於激活狀態,那麼每一個事務都會須要等待日誌同步,而且每次事務提交都要求兩第二天志重寫,而不是一次。換句話說,若是想讓事務和二進制日誌安全地同步,就會要求至少三次fsync()調用。防止其發生的惟一辦法就是禁用二進制日誌並把innodb_support_xa設置爲0。
這樣設置沒法兼容複製。複製須要二進制日誌和XA支持,而且爲了儘量地安全,還需要把sync_binlog設置成1,這樣設置就能對存儲引擎和二進制日誌進行同步。(不然的話,XA支持就沒有必要了,由於二進制日誌不會被提交到磁盤上)。這是強烈推薦使用帶有備用電池的寫入緩存的磁盤陣列控制器的一個緣由,它能加快fsync()調用而且恢復性能。
下一章將會詳細講解如何配置事務日誌及二進制日誌。
5.11.2 外部XA事務
External XA Transactions
MySQL能夠參與,但不能管理外部分佈式事務。它不支持完整的XA規範。例如,XA規範容許鏈接運行單個事務中的鏈接,可是MySQL如今還不能作到這一點。
外部XA事務的開銷比內部XA事務更高,這是由於延遲會增長,而且參與者失敗的可能性更大。在WAN、甚至是因特網上使用XA,一個常見的問題就是網絡性能不可預測。當有不可預測的部分,好比較慢的網絡或一個有可能好久都不點擊"保存"按鈕的用戶,最好的選擇就是避免XA事務。任何耽擱提交的因素都會有很高的代價,由於它致使的不是單個系統延遲,而是許多系統。
能夠用另外的方式設計分佈式事務。例如,能夠在本地把數據插入隊列,而後把它自動地分佈成小而快的事務。也可使用MySQL複製把數據從一個地方搬運到另一個地方。咱們也發現某些使用了分佈式事務的應用程序其實根本不必使用事務。
總的說來,XA事務是一種在服務器之間同步數據的有用的方式。若是由於某些緣由,好比不能使用複製或數據更新的性能並非關鍵因素,它的效果會不錯。
綜上所述,分佈式XA事務還很差用,問題多,性能也很差,想一想使用其餘替代方案吧。