XA 本身
關(guān)于xa本身不會(huì)過多介紹(因?yàn)槲乙彩强磩e人寫的啊),可以兩個(gè)方向去搜索
- 【官方】xa規(guī)范官方文檔和MySQL xa官方文檔
- 【民間】別人已經(jīng)學(xué)習(xí)過記錄的博客
可以閱讀 http://www.reibang.com/p/7003d58ea182 刊侯,我就用這個(gè)文章學(xué)習(xí)的
拓?fù)浣Y(jié)構(gòu)
借用http://www.reibang.com/p/7003d58ea182里的圖片
在DBLE這樣的分庫分表中間里紧武,拓?fù)浣Y(jié)構(gòu)其實(shí)是這樣的函匕。
DBLE Server進(jìn)程是AP,TM是作為代碼邏輯模塊內(nèi)嵌在DBLE Server進(jìn)程內(nèi)部的疹味。DBLE Server使用MySQL xa事務(wù)sql語法去定義事務(wù)邊界。RM是分庫分表背后的MySQL房交。
MySQL XA SQL語法
https://dev.mysql.com/doc/refman/5.7/en/xa-statements.html
XA {START|BEGIN} xid [JOIN|RESUME]
XA END xid [SUSPEND [FOR MIGRATE]]
XA PREPARE xid
XA COMMIT xid [ONE PHASE]
XA ROLLBACK xid
XA RECOVER [CONVERT XID]
- XA START和XA END用于確定一個(gè)分支事務(wù)的邊界
- XA PREPARE 和XA COMMIT彻舰,XA ROLLBACK用來做2PC
- XA RECOVER是管控命令,用來查看一階段prepare過但是還沒二階段commit或者rollback的分支事務(wù)
XA事務(wù)狀態(tài)變遷圖
借用http://www.reibang.com/p/7003d58ea182里的XA事務(wù)狀態(tài)變遷
這個(gè)圖需要注意的地方是
(1)如果你xa start開啟一個(gè)全局事務(wù)以后候味,你不走到兩個(gè)終點(diǎn)刃唤,你是沒法繼續(xù)開啟下一個(gè)xa事務(wù)的。即使你xa start后什么DML也沒執(zhí)行白群,你也需要按照xa end, xa rollback的流程這樣結(jié)束事務(wù)尚胞,否則XAER_RMFAIL: The command cannot be executed when global transaction is in the %s state
這樣的報(bào)錯(cuò)就會(huì)來敲門。
(2)當(dāng)分布式事務(wù)里只有一個(gè)RM帜慢,即分布式事務(wù)退化成本地事務(wù)了笼裳,xa commit one phase 時(shí)會(huì)將prepare和commit 一起完成。 DBLE里面并沒有使用這種優(yōu)化粱玲!
再次借用http://www.reibang.com/p/7003d58ea182里的圖躬柬,這樣只有四種到最終狀態(tài)的路徑。
關(guān)注xa prepare
在xa prepare執(zhí)行成功之前抽减,如果你關(guān)閉到MySQL的鏈接允青,事務(wù)會(huì)被MySQL自動(dòng)回滾的,不會(huì)留下任何副作用胯甩。
但是在xa prepare執(zhí)行成功之后昧廷,你必須決定commit還是rollback,并且按照上圖的流程圖走到結(jié)束狀態(tài)偎箫。因?yàn)閤a prepare執(zhí)行成功之后木柬,事務(wù)信息將被持久化,不管連接斷開還是MySQL Server重啟淹办,事務(wù)都將被重新恢復(fù)(通過information_schema.innodb_trx你可以查詢到)眉枕,這些事務(wù)占有的鎖都會(huì)阻止其他會(huì)產(chǎn)生鎖沖突的事務(wù)繼續(xù)執(zhí)行。這是和MySQL本地事務(wù)不同的地方怜森。通過xa recover可以查詢到這些xa prepare過的事務(wù)速挑。
dble官方文檔的圖片中也說到這些
binlog
binlog會(huì)記錄xa事務(wù)的日志
個(gè)人發(fā)現(xiàn)
- 只有xa prepare執(zhí)行后才會(huì)記錄binlog,但是會(huì)記錄到此為止的所有語句副硅。
- 沒實(shí)際修改到數(shù)據(jù)的DML不會(huì)記錄binlog
DBLE XA實(shí)現(xiàn)分析
https://actiontech.github.io/dble-docs-cn/2.Function/2.05_distribute_transaction.html
可以先閱讀dble xa事務(wù)的文檔姥宝,建立一些術(shù)語概念
分庫分表中間件用XA事務(wù) 解決跨庫事務(wù)的思路
分庫分表中間件對(duì)用戶來說就像一個(gè)MySQL,分庫分表中間件自己面對(duì)多個(gè)MySQL恐疲。
用戶發(fā)出的邏輯sql腊满,會(huì)被中間件解析成多條到不同MySQL的物理sql。
在中間件不使用任何分布式事務(wù)解決方案的時(shí)候培己,只是在多個(gè)MySQL上執(zhí)行本地事務(wù)碳蛋,和用戶自己連接到MySQL上執(zhí)行本地事務(wù)一致。這種最大努力嘗試提交的方式是無法保證數(shù)據(jù)一致性的省咨。
無論中間件使用xa事務(wù)還是使用本地事務(wù)肃弟,都是分庫分表中間件內(nèi)部自己使用的事務(wù),業(yè)務(wù)代碼訪問分庫分表中間件仍然使用的是普通本地事務(wù)零蓉,這也是分庫分表中間件一直在宣傳的(有限)透明, 對(duì)業(yè)務(wù)代碼侵入低笤受。
這樣去想,就知道分庫分表中間件在用xa解決跨庫事務(wù)的時(shí)候要做到的流程
業(yè)務(wù)側(cè)連接到中間件執(zhí)行本地事務(wù)的一般步驟
- 顯式事務(wù)
start transaction / begin
DML
commit/rollback
- 隱式事務(wù)
set autocommit=0;
DML
commit/rollback
- autocommit開啟時(shí)的單條的跨庫sql
autocommit為true的單條sql原子事務(wù)
delete from xa_test where id in (1,2,3);
分庫分表中間件接受到用戶本地事務(wù)命令的反應(yīng)
以顯式事務(wù)為例
- start transaction / begin
中間件設(shè)置前端連接的事務(wù)開始標(biāo)志 - DML
邏輯DML可能翻譯成一條或多條物理DML
使用相關(guān)物理分庫對(duì)應(yīng)的后端連接壁公,中間件執(zhí)行xa start和物理DML語句 - commit/rollback
使用相關(guān)物理分庫對(duì)應(yīng)的后端連接感论,執(zhí)行xa end,xa preapre, xa commit或者xa rollback, 結(jié)束后清理前段連接的事務(wù)開始標(biāo)志
源碼結(jié)構(gòu)
com.actiontech.dble.backend.mysql.nio.handler.transaction.xa
xa事務(wù)控制語句執(zhí)行后的回調(diào)紊册,負(fù)責(zé)推動(dòng)流程進(jìn)行
com.actiontech.dble.backend.mysql.nio.handler.transaction.xa.XACommitNodesHandler
com.actiontech.dble.backend.mysql.nio.handler.transaction.xa.XARollbackNodesHandler
com.actiontech.dble.backend.mysql.nio.handler.transaction.xa.XAAutoCommitNodesHandler
com.actiontech.dble.backend.mysql.nio.handler.transaction.xa.XAAutoRollbackNodesHandler
com.actiontech.dble.backend.mysql.xa.TxState
XA事務(wù)狀態(tài)定義比肄,注意里面的定義既包括全局事務(wù)的狀態(tài),也包括分支事務(wù)的狀態(tài)定義
- ing結(jié)尾的就是sql執(zhí)行剛開始
- (start,end,prepare,commit,rollback)ed結(jié)尾的是明確執(zhí)行成功(或者失敗了也沒副作用的場景)
- ed結(jié)尾有UNCONNECT后綴的是網(wǎng)絡(luò)錯(cuò)誤導(dǎo)致執(zhí)行結(jié)果未知的場景
- FAILED就是執(zhí)行失敗狀態(tài)的囊陡,按照前面說的芳绩,必須要重試變成commited或者rollbacked
com.actiontech.dble.backend.mysql.xa.recovery
事務(wù)日志,存本地還是zk撞反,server宕機(jī)后啟動(dòng)的事務(wù)中斷恢復(fù)邏輯
客戶端連接DBLE Server代碼示例
下面只是顯式事務(wù)的例子妥色,正常使用jdbc你應(yīng)該不會(huì)這樣寫。
DBLE需要用戶額外執(zhí)行一個(gè)set xa=1遏片,來告訴中間件接下來的sql嘹害,中間件需要使用xa事務(wù)撮竿。如果不執(zhí)行set xa=1, dble將在每個(gè)MySQL上使用本地事務(wù)。
也可以通過在jdbc url里面增加參數(shù)(jdbc:mysql://127.0.0.1:8066?sessionVariables=xa=1)來避免顯式執(zhí)行set xa=1以減少侵入性笔呀。
Statement stmt = conn.createStatement();
stmt.execute("set xa = 1");
stmt.execute("begin");
try {
stmt.executeUpdate("delete from xa_test;");
stmt.execute("commit");
} catch (Exception e) {
e.printStackTrace();
stmt.execute("rollback");
} finally {
conn.close();
}
詳細(xì)執(zhí)行流程
先上圖來些印象幢踏,這個(gè)圖可能不是太正規(guī),我只是按照便于我理解的思路去畫的许师。
主要畫的是事務(wù)狀態(tài)的流轉(zhuǎn)
- 全局事務(wù)狀態(tài) com.actiontech.dble.server.NonBlockingSession#xaState
- 分支事務(wù)狀態(tài) com.actiontech.dble.backend.mysql.nio.MySQLConnection#xaStatus
顯式事務(wù) commit成功
顯式事務(wù) rollback成功
代碼流程分析
1. set xa=1;
com.actiontech.dble.server.handler.SetHandler#handleSingleXA
如果之前xa事務(wù)開啟標(biāo)志沒打開的話房蝉,這里打開會(huì)重新生成一次xid。
所以如果打開以后微渠,只要不關(guān)閉再打開搭幻,dble的一個(gè)前端連接,一直使用的是相同的xid.
1.1 xid的生成
在MySQL官方文檔中逞盆,關(guān)于xid的組成也有類似的說明:
xid: gtrid [, bqual [, formatID ]]其中檀蹋,bqual、formatID是可選的云芦。解釋如下:
gtrid : 是一個(gè)全局事務(wù)標(biāo)識(shí)符(global transaction identifier)续扔,
bqual:是一個(gè)分支限定符(branch qualifier),如果沒有提供bqual焕数,那么默認(rèn)值為空字符串''纱昧。
formatID:是一個(gè)數(shù)字,用于標(biāo)記gtrid和bqual值的格式堡赔,這是一個(gè)無符號(hào)整數(shù)(unsigned integer)识脆,也就是說,最小為0善已。如果沒有提供formatID灼捂,那么其默認(rèn)值為1。
com.actiontech.dble.DbleServer#genXaTxId
DBLE是只使用了gtrid的部分换团,感覺有些不規(guī)范悉稠。但是因?yàn)閤id傳到了MySQL的時(shí)候,MySQL只是個(gè)分支事務(wù)艘包,所以每個(gè)分支事務(wù)的xid不同的猛,只要TM自己知道哪些xid是一個(gè)全局事務(wù)里的就行了。按照規(guī)范來的好處我覺得是方便人工去判斷某個(gè)分支事務(wù)是哪個(gè)全局事務(wù)想虎,而不需要到TM的事務(wù)日志里去查詢卦尊。
zk集群模式下的全局事務(wù)xid是
'Dble_Server.{myid}.{進(jìn)程內(nèi)原子自增數(shù)}'
實(shí)際在執(zhí)行xa start的時(shí)候使用的分支事務(wù)xid是會(huì)在加上物理庫的名稱
'Dble_Server.{myid}.{進(jìn)程內(nèi)原子自增數(shù)}.{schema}'
保證了每個(gè)前端連接的xid在DBLE Server集群內(nèi)是唯一的。
2. begin/start transaction
com.actiontech.dble.server.handler.BeginHandler#handle
begin/start transaction是在單次事務(wù)里關(guān)閉autocommit模式舌厨,這意味著岂却,在本地事務(wù)執(zhí)行結(jié)束后,事務(wù)前的autocommit還要被恢復(fù)回來。后面的細(xì)節(jié)躏哩。
3. DML
- com.actiontech.dble.backend.mysql.nio.MySQLConnection#synAndDoExecute
回調(diào)函數(shù)是com.actiontech.dble.backend.mysql.nio.handler.SingleNodeHandler
對(duì)應(yīng)用戶本地事務(wù)中某條不跨庫的sql
set xa=1;
begin;
insert into xa_test (id) values (1);
insert into xa_test (id) values (2);
commit;
- com.actiontech.dble.backend.mysql.nio.MySQLConnection#synAndDoExecuteMultiNode
回調(diào)函數(shù)是com.actiontech.dble.backend.mysql.nio.handler.MultiNodeQueryHandler
用戶本地事務(wù)中某條跨庫的sql
set xa=1;
begin;
insert into xa_test (id) values (1,2);
commit;
這兩個(gè)方法需要關(guān)注com.actiontech.dble.backend.mysql.nio.MySQLConnection.StatusSync這個(gè)類署浩。
除了DML本身,dble會(huì)把事務(wù)控制語句扫尺,和set autocommit這些語句拼在DML前面瑰抵,然后在一次請求里發(fā)送給MySQL。
XA START 'Dble_Server.server_01.791.db2';INSERT INTO xa_test
VALUES (1);
需要注意器联,這樣拼接多個(gè)SQL一起發(fā)過去的時(shí)候,如果中間某個(gè)sql出錯(cuò)婿崭,后續(xù)sql的響應(yīng)是不會(huì)收到的拨拓。MultiNodeQueryHandler#errorResponse的處理是不當(dāng)?shù)模瑫?huì)導(dǎo)致程會(huì)話hang住氓栈,詳見我提的issue https://github.com/actiontech/dble/issues/1164
4. 顯式的commit或者rollback
com.actiontech.dble.backend.mysql.nio.handler.transaction.xa.XACommitNodesHandler
com.actiontech.dble.backend.mysql.nio.handler.transaction.xa.XARollbackNodesHandler
回調(diào)函數(shù)需要關(guān)注分布式系統(tǒng)調(diào)用的三態(tài)
事務(wù)控制語句執(zhí)行結(jié)果未知
ResponseHandler#connectionError
ResponseHandler#connectionClose事務(wù)控制語句執(zhí)行明確失敗
ResponseHandler#errorResponse事務(wù)控制語句執(zhí)行明確成功
ResponseHandler#okResponse
這里我并不想仔細(xì)分析渣磷,你可以看我上面的事務(wù)狀態(tài)流程圖,也可以理解授瘦。自己調(diào)試一遍才對(duì)狀態(tài)流轉(zhuǎn)有個(gè)感受醋界。
4.1 handler的復(fù)用
一般來說dble里面回調(diào)函數(shù)是每次執(zhí)行sql時(shí)創(chuàng)建一個(gè)對(duì)象,執(zhí)行完就釋放了提完⌒畏模回調(diào)函數(shù)只會(huì)被使用一次。
但是XACommitNodesHandler和XARollbackNodesHandler是被多次復(fù)用的徒欣,因?yàn)樵谟脩魣?zhí)行commit或者rollback后逐样,dble需要執(zhí)行多次sql,例如xa end,xa prepare,xa commit打肝。
handler的復(fù)用需要仔細(xì)的清除上次sql執(zhí)行遺留的狀態(tài)脂新,否則可能會(huì)影響下次xa事務(wù)的執(zhí)行,詳見我提的issue https://github.com/actiontech/dble/issues/1156 ,PS: issue提到的問題是我自己解決前面問題后碰到的粗梭,正常代碼可能沒有場景復(fù)現(xiàn)争便,但是這里確實(shí)是一個(gè)代碼邏輯問題。
4.2 事務(wù)控制語句所有節(jié)點(diǎn)全部發(fā)送完才開始處理響應(yīng)
對(duì)于某個(gè)流程控制語句断医,例如xa end xid這種滞乙,必須所有MySQL都發(fā)送完成后,回調(diào)函數(shù)才能處理響應(yīng)鉴嗤。
發(fā)送時(shí)候是遍歷所有分支事務(wù)去發(fā)送語句的酷宵,但是先收到響應(yīng)的人必須等所有人都發(fā)送完成后,才能開始處理響應(yīng)躬窜。
通過在XACommitNodesHandler#waitUntilSendFinish和XARollbackNodesHandler#waitUntilSendFinish上面阻塞實(shí)現(xiàn)的浇垦。
我在測試某個(gè)【從DBLE遷移XA功能】的MyCAT分支代碼時(shí)發(fā)現(xiàn), 如果只遷移DBLE XA部分本身的邏輯,DBLE的發(fā)送細(xì)節(jié)會(huì)造成一些危險(xiǎn)荣挨。在遍歷發(fā)送XA控制語句結(jié)束前男韧,如果后端連接出現(xiàn)網(wǎng)絡(luò)斷開朴摊,io.mycat.backend.mysql.nio.MySQLConnection.close會(huì)在發(fā)送線程里觸發(fā)回調(diào)函數(shù)。因?yàn)樗蠿A事務(wù)控制語句 全部發(fā)送完畢才會(huì)調(diào)用Condition#signalAll此虑,所以發(fā)送線程卡死在回調(diào)函數(shù)的waitUntilSendFinish方法上甚纲。
DBLE里面關(guān)閉連接是異步調(diào)用回調(diào)函數(shù)的(com.actiontech.dble.backend.mysql.nio.MySQLConnection#closeResponseHandler),所以沒有MyCAT里面的這個(gè)卡死問題朦前。
卡住的線程堆棧如下
"BusinessExecutor6@3827" daemon prio=5 tid=0x47 nid=NA waiting
java.lang.Thread.State: WAITING
at sun.misc.Unsafe.park(Unsafe.java:-1)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:186)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2043)
at io.mycat.backend.mysql.nio.handler.transaction.xa.XACommitNodesHandler.waitUntilSendFinish(XACommitNodesHandler.java:515)
at io.mycat.backend.mysql.nio.handler.transaction.xa.XACommitNodesHandler.connectionClose(XACommitNodesHandler.java:377)
at io.mycat.backend.mysql.nio.MySQLConnection.close(MySQLConnection.java:611)
at io.mycat.net.NIOSocketWR.doNextWriteCheck(NIOSocketWR.java:60)
at io.mycat.net.AbstractConnection.write(AbstractConnection.java:454)
at io.mycat.net.mysql.CommandPacket.write(CommandPacket.java:122)
at io.mycat.backend.mysql.nio.MySQLConnection.sendQueryCmd(MySQLConnection.java:308)
at io.mycat.backend.mysql.nio.MySQLConnection.execCmd(MySQLConnection.java:665)
at io.mycat.backend.mysql.nio.handler.transaction.xa.XACommitNodesHandler.endPhase(XACommitNodesHandler.java:196)
at io.mycat.backend.mysql.nio.handler.transaction.xa.XACommitNodesHandler.executeCommit(XACommitNodesHandler.java:128)
at io.mycat.backend.mysql.nio.handler.transaction.xa.XACommitNodesHandler.commit0(XACommitNodesHandler.java:91)
at io.mycat.backend.mysql.nio.handler.transaction.xa.XACommitNodesHandler.access$000(XACommitNodesHandler.java:31)
at io.mycat.backend.mysql.nio.handler.transaction.xa.XACommitNodesHandler$1.run(XACommitNodesHandler.java:59)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at java.lang.Thread.run(Thread.java:745)
4.3 出錯(cuò)處理
(1)丟棄后端連接
前面說過xa prepare執(zhí)行成功之前介杆,丟棄后端連接都可以讓MySQL自動(dòng)回滾事務(wù),下面回調(diào)函數(shù)方法大多這樣處理的韭寸。
MultiNodeQueryHandler#errorResponse
XACommitNodesHandler#errorResponse
XARollbackNodesHandler#errorResponse
但是個(gè)人感覺MultiNodeQueryHandler#errorResponse里面春哨,如果DML執(zhí)行報(bào)錯(cuò)的話,是否不用丟棄后端連接恩伺?執(zhí)行XA END和XA ROLLBACK, 可以保留一個(gè)后端連接赴背。
(2)重新獲取后端連接
xa prepare以及其后階段的事務(wù)控制語句,執(zhí)行時(shí)出現(xiàn)網(wǎng)絡(luò)斷開的時(shí)候晶渠,后端連接已經(jīng)不可用了凰荚,但是事務(wù)語句可能會(huì)執(zhí)行成功,只是DBLE Server沒有接收到響應(yīng)褒脯,所以必須重新獲取一個(gè)后端連接去做某些操作便瑟,rollback或者明確事務(wù)結(jié)果。
這里刷新調(diào)用的是com.actiontech.dble.server.NonBlockingSession#freshConn
需要注意的是番川,不只是老連接關(guān)閉的場景下回走到NonBlockingSession#freshConn胳徽,老的連接沒關(guān)閉的時(shí)候也會(huì)使用NonBlockingSession#freshConn。而NonBlockingSession#freshConn會(huì)用新的鏈接替換老連接在com.actiontech.dble.server.NonBlockingSession#target里的鍵值對(duì)爽彤,session本身只會(huì)清理NonBlockingSession#target里的連接养盗,如果freshConn的時(shí)候不釋放老的鏈接,可能會(huì)造成連接泄露适篙。從單獨(dú)使用NonBlockingSession#freshConn替換連接的語義上來說往核,freshConn方法可以直接把老的鏈接給釋放掉。詳見https://github.com/actiontech/dble/issues/1158
(3)xa prepare執(zhí)行成功后 commit或者rollback的重試
前面說過xa prepare沒執(zhí)行成功前嚷节,關(guān)閉后端連接是個(gè)快刀斬亂麻的解決方式聂儒。
但當(dāng)某個(gè)分支事務(wù)的xa prepare執(zhí)行成功后,則必須成功執(zhí)行xa rollback或者xa commit硫痰。
DBLE這里處理策略是衩婚,在一定次數(shù)內(nèi)去重試xa commit或者xa rollback,如果成功了效斑,用戶不會(huì)有任何察覺非春,用戶只是覺得commit或者rollback有點(diǎn)慢。
但是如果一定次數(shù)全部都沒重試成功,那么dble會(huì)斷開前端連接奇昙。這里是正確的處理方式护侮,當(dāng)用戶通過jdbc執(zhí)行commit或者rollback發(fā)生連接斷開的時(shí)候,MySQL JDBC驅(qū)動(dòng)會(huì)拋com.mysql.jdbc.SQLError#SQL_STATE_TRANSACTION_RESOLUTION_UNKNOWN差異储耐,告知用戶事務(wù)結(jié)果不明 Communications link failure during commit()/rollback(). Transaction resolution unknown.
羊初。
之后DBLE Server會(huì)后臺(tái)無限重試commit或者rollback,確保事務(wù)一定成功commit或者rollback.
在manager端口通過show @@session.xa命令什湘,可以查詢到在進(jìn)行提交和回滾的事務(wù)长赞。
com.actiontech.dble.manager.response.ShowXASession#execute
理解全局事務(wù)狀態(tài)和分支事務(wù)狀態(tài)的關(guān)系
com.actiontech.dble.backend.mysql.xa.TxState里事務(wù)狀態(tài),有的是中間失敗狀態(tài)闽撤,有的是終點(diǎn)成功狀態(tài)得哆。
在dble代碼里
只要有一個(gè)分支事務(wù)失敗的時(shí)候,分支事務(wù)的失敗狀態(tài)也會(huì)設(shè)置到全局事務(wù)的狀態(tài)上去腹尖。
所有分支事務(wù)都被設(shè)置成某個(gè)成功狀態(tài)的時(shí)候,全局事務(wù)的狀態(tài)才有可能會(huì)被設(shè)置成分支事務(wù)的成功的狀態(tài)伐脖。
這樣就知道NonBlockingSession#releaseExcept方法的邏輯了热幔。
例如XACommitNodesHandler#cleanAndFeedback里面, 全局事務(wù)是個(gè)失敗的狀態(tài),那么必然是從某個(gè)分支事務(wù)的失敗狀態(tài)來的讼庇,如果還有分支事務(wù)狀態(tài)和全局事務(wù)狀態(tài)一樣绎巨,那么還有人沒重試成功。
} else if (session.getXaState() == TxState.TX_COMMIT_FAILED_STATE) {
// 還有分支事務(wù)和全局事務(wù)的失敗狀態(tài)一致的么
MySQLConnection errConn = session.releaseExcept(TxState.TX_COMMIT_FAILED_STATE);
if (errConn != null) {
// 還有人沒有執(zhí)行成功蠕啄,繼續(xù)重試
......
} else {
// 大家都成功了场勤,收工了
......
}
}
不按套路出牌的用戶如何處理
正常情況我們用jdbc事務(wù)的時(shí)候,如果DML執(zhí)行出錯(cuò)歼跟,拋出異常和媳,我們會(huì)在catch語句里進(jìn)行rollback。
但是如果用戶不僅不rollback哈街,還去commit或者執(zhí)行其他語句怎么辦留瞳?
com.actiontech.dble.server.ServerConnection#txInterrupted 前端連接有個(gè)flag, 如果執(zhí)行失敗,需要回滾的時(shí)候骚秦,這個(gè)標(biāo)志會(huì)被打開她倘,下面再進(jìn)行任何非rollback的語句都會(huì)將com.actiontech.dble.server.ServerConnection#txInterruptMsg的報(bào)錯(cuò)信息返回給用戶。
顯式XA事務(wù) 部分場景下的commit失敗時(shí)會(huì)自動(dòng)回滾
正常來說作箍,XAAutoCommitNodesHandler和XAAutoRollbackNodesHandler只會(huì)被用在:
在autocommit為true,啟用xa事務(wù)的前端連接(set xa=1)里硬梁,用戶執(zhí)行了一條跨庫的sql,因?yàn)閍utocommit開啟的時(shí)候胞得,單條sql就是一個(gè)原子事務(wù)荧止,所以這條sql執(zhí)行過程里dble會(huì)走xa流程,進(jìn)行自動(dòng)提交或者自動(dòng)回滾。
但是代碼里的有的地方罩息,在顯式事務(wù)的時(shí)候嗤详,如果用戶執(zhí)行commit, DBLE Server執(zhí)行xa prepare出現(xiàn)網(wǎng)絡(luò)斷開的時(shí)候瓷炮,dble會(huì)自動(dòng)回滾事務(wù)葱色。
代碼在com.actiontech.dble.backend.mysql.nio.handler.transaction.xa.XACommitNodesHandler#nextParse,非TX_PREPARE_UNCONNECT_STATE的狀態(tài)娘香,由客戶之后的rollback進(jìn)行回滾苍狰。
TX_PREPARE_UNCONNECT_STATE的會(huì)自動(dòng)進(jìn)行回滾,之后客戶執(zhí)行rollback的時(shí)候烘绽,其實(shí)事務(wù)已經(jīng)執(zhí)行結(jié)束了淋昭,所以rollback只是個(gè)過場。
protected void nextParse() {
if (this.isFail() && session.getXaState() != TxState.TX_PREPARE_UNCONNECT_STATE) {
session.getSource().setTxInterrupt(error);
session.getSource().write(sendData);
LOGGER.info("nextParse failed:" + error);
} else {
commit();
}
}
這里的自動(dòng)回滾我一開始沒有理解安接,加上https://github.com/actiontech/dble/issues/1170里面自動(dòng)回滾沒有把真實(shí)出錯(cuò)原因返回給用戶翔忽,而是直接返回OK,所以我當(dāng)時(shí)認(rèn)為應(yīng)該讓客戶自己rollback盏檐。
后來對(duì)比MySQL本地事務(wù)歇式,MySQL什么時(shí)候會(huì)commit失敗,MySQL commit失敗的時(shí)候會(huì)不會(huì)自動(dòng)進(jìn)行回滾胡野?如果sql全部執(zhí)行成功材失,我百度不到MySQL commit會(huì)失敗的場景。但是在中間件xa事務(wù)的時(shí)候硫豆,commit由很多步驟組成龙巨,所以commit會(huì)出現(xiàn)失敗場景,在commit失敗的時(shí)候熊响,中間件進(jìn)行回滾旨别,這樣看來是可以接受的。
事務(wù)執(zhí)行過程中汗茄,前端連接被關(guān)閉
如果用戶在執(zhí)行xa事務(wù)的過程中昼榛,用kill connection id去強(qiáng)殺前端連接,DBLE Server應(yīng)該如何處理?
如果會(huì)話執(zhí)行過程中剔难,用戶和DBLE Server的鏈接發(fā)生斷開如何處理? (例如jdbc statement timeout, jdbc connection 的socket timeout等胆屿,他們統(tǒng)統(tǒng)都是下面這樣處理的。)
強(qiáng)殺和前他原因?qū)е碌那岸诉B接關(guān)閉 分別在com.actiontech.dble.server.ServerConnection#killAndClose和com.actiontech.dble.server.ServerConnection#close
以ServerConnection#killAndClose為例
@Override
public void killAndClose(String reason) {
if (session.getSource().isTxStart() && !session.cancelableStatusSet(NonBlockingSession.CANCEL_STATUS_CANCELING) &&
session.getXaState() != null && session.getXaState() != TxState.TX_INITIALIZE_STATE) {
//XA transaction in this phase(commit/rollback) close the front end and wait for the backend finished
super.close(reason);
} else {
//not a xa transaction ,close it
super.close(reason);
session.kill();
}
}
第一個(gè)判斷邏輯
(1)session.getSource().isTxStart() // XA事務(wù)開啟偶宫,顯式begin/start transaction或者autocommit=false執(zhí)行DML過
(2)session.getXaState() != null && session.getXaState() != TxState.TX_INITIALIZE_STATE //執(zhí)行過DML
(3)!session.cancelableStatusSet(NonBlockingSession.CANCEL_STATUS_CANCELING)
注意上面的第(3)個(gè)條件非迹,可以看下com.actiontech.dble.server.NonBlockingSession#cancelableStatusSet在什么時(shí)候會(huì)被調(diào)用
com.actiontech.dble.backend.mysql.nio.handler.transaction.xa.XARollbackNodesHandler#rollback
if (session.getXaState() != null &&
session.getXaState() == TxState.TX_ENDED_STATE) {
if (!session.cancelableStatusSet(NonBlockingSession.CANCEL_STATUS_COMMITTING)) {
return;
}
}
com.actiontech.dble.backend.mysql.nio.handler.transaction.xa.XACommitNodesHandler#commit
if (session.getXaState() != null && session.getXaState() == TxState.TX_ENDED_STATE) {
if (!session.cancelableStatusSet(NonBlockingSession.CANCEL_STATUS_COMMITTING)) {
return;
}
}
這里達(dá)到的效果是是,只有全局事務(wù)在commit或者rollback過程中還沒進(jìn)入到xa end執(zhí)行之后的階段時(shí)(xa prepare纯趋,xa commit,xa rollback這些)憎兽,才會(huì)在關(guān)閉前端連接的同時(shí)冷离,也關(guān)閉后端連接。否則只會(huì)關(guān)閉前端連接纯命,后端連接繼續(xù)執(zhí)行事務(wù)流程西剥。
事務(wù)日志和事務(wù)恢復(fù)
事務(wù)日志
DBLE Server側(cè)的事務(wù)日志非常重要,因?yàn)槊總€(gè)MySQL只是分支事務(wù)參與者亿汞,在DBLE Server宕機(jī)后瞭空,僅從MySQL側(cè)是無法知道全局事務(wù)在二階段時(shí)是決議提交還是決議回滾的。
事務(wù)日志主要記錄了全局事務(wù)(CoordinatorLogEntry)和其所包含的分支事務(wù)(ParticipantLogEntry)的信息(這樣xid就算隨便起也沒事疗我,這里TM知道對(duì)應(yīng)關(guān)系)
主要關(guān)注下事務(wù)狀態(tài)
- 事務(wù)日志中的全局事務(wù)狀態(tài) com.actiontech.dble.backend.mysql.xa.CoordinatorLogEntry#setTxState
- 事務(wù)日志中的分支事務(wù)狀態(tài) com.actiontech.dble.backend.mysql.xa.ParticipantLogEntry#setTxStat
更新全局事務(wù)信息
(1) com.actiontech.dble.backend.mysql.xa.XAStateLog#saveXARecoveryLog(java.lang.String, com.actiontech.dble.backend.mysql.xa.TxState)
更新分支事務(wù)信息
(1) com.actiontech.dble.backend.mysql.xa.XAStateLog#saveXARecoveryLog(java.lang.String, com.actiontech.dble.backend.mysql.nio.MySQLConnection)
(2) com.actiontech.dble.backend.mysql.xa.XAStateLog#updateXARecoveryLog(java.lang.String, java.lang.String, int, java.lang.String, com.actiontech.dble.backend.mysql.xa.TxState)
刷盤時(shí)機(jī)
只有更新全局事務(wù)信息的時(shí)候才會(huì)刷盤咆畏,但并不是每次更新都會(huì)刷盤,大部分時(shí)候都只是修改內(nèi)存里的值吴裤。
com.actiontech.dble.backend.mysql.xa.XAStateLog#saveXARecoveryLog(java.lang.String, com.actiontech.dble.backend.mysql.xa.TxState)
public static boolean saveXARecoveryLog(String xaTxId, TxState sessionState) {
CoordinatorLogEntry coordinatorLogEntry = IN_MEMORY_REPOSITORY.get(xaTxId);
coordinatorLogEntry.setTxState(sessionState);
flushMemoryRepository(xaTxId, coordinatorLogEntry);
if (DbleServer.getInstance().getConfig().getSystem().getUsePerformanceMode() == 1) {
return true;
}
//will preparing, may success send but failed received,should be rollback
if (sessionState == TxState.TX_PREPARING_STATE ||
//will committing, may success send but failed received,should be commit agagin
sessionState == TxState.TX_COMMITTING_STATE ||
//will rollbacking, may success send but failed received,should be rollback agagin
sessionState == TxState.TX_ROLLBACKING_STATE) {
return writeCheckpoint(xaTxId);
}
return true;
}
這樣的刷盤時(shí)機(jī)是可能導(dǎo)致一些問題旧找,詳見我提的issue https://github.com/actiontech/dble/issues/1192,主要是內(nèi)存里的事務(wù)狀態(tài)還沒被刷到磁盤的時(shí)候server宕機(jī)麦牺,server啟動(dòng)恢復(fù)的時(shí)候需要順著流程往前多判斷一些事務(wù)狀態(tài)钮蛛,否則會(huì)漏恢復(fù)prepare過的事務(wù),造成事務(wù)泄露剖膳。
事務(wù)恢復(fù)
server如果宕機(jī)了魏颓,DBLE Server在重啟的時(shí)候會(huì)進(jìn)行恢復(fù)。
PS: 其實(shí)覺得如果有server宕機(jī)了潮秘,在zk集群的時(shí)候琼开,其他存活的節(jié)點(diǎn)能否接管進(jìn)行事務(wù)恢復(fù)易结?
什么樣的中斷事務(wù)需要恢復(fù)枕荞?
本質(zhì)上server宕機(jī)時(shí),在顯式/隱式 提交/回滾的時(shí)候搞动,事務(wù)狀態(tài)都是未知的躏精,這樣server這里只要根據(jù)事務(wù)流程進(jìn)行到的階段,除非數(shù)據(jù)一致性必須要commit的場景才重試xa commit全局事務(wù)(事務(wù)原子性)鹦肿,否則就進(jìn)行全局事務(wù)回滾(釋放鎖之類的)矗烛。
準(zhǔn)確說,任何一個(gè)分支事務(wù)只要xa prepare可能執(zhí)行成功了箩溃,但是還沒有明確成功執(zhí)行過xa commit或者xa rollback的話瞭吃,就要在啟動(dòng)的時(shí)候進(jìn)行恢復(fù)。
這點(diǎn)從com.actiontech.dble.DbleServer#performXARecoveryLog里面可以看出來涣旨,先判斷全局事務(wù)狀態(tài)歪架,決定全局事務(wù)是否需要提交或者回滾。這里的狀態(tài)全部都是xa prepare可能執(zhí)行成功過的霹陡。
// XA recovery log check
private void performXARecoveryLog() {
...........
for (CoordinatorLogEntry coordinatorLogEntry : coordinatorLogEntries) {
boolean needRollback = false;
boolean needCommit = false;
if (coordinatorLogEntry.getTxState() == TxState.TX_COMMIT_FAILED_STATE ||
// will committing, may send but failed receiving, should commit agagin
coordinatorLogEntry.getTxState() == TxState.TX_COMMITTING_STATE) {
needCommit = true;
} else if (coordinatorLogEntry.getTxState() == TxState.TX_ROLLBACK_FAILED_STATE ||
//don't konw prepare is succeed or not ,should rollback
coordinatorLogEntry.getTxState() == TxState.TX_PREPARE_UNCONNECT_STATE ||
// will rollbacking, may send but failed receiving,should rollback again
coordinatorLogEntry.getTxState() == TxState.TX_ROLLBACKING_STATE ||
// will preparing, may send but failed receiving,should rollback again
coordinatorLogEntry.getTxState() == TxState.TX_PREPARING_STATE) {
needRollback = true;
}
if (needCommit || needRollback) {
tryRecovery(coordinatorLogEntry, needCommit);
}
}
}
dble現(xiàn)在的判斷邏輯是寧濫毋缺的和蚪,可能不需要回滾的場景止状,也會(huì)回滾的,但是絕對(duì)不會(huì)漏掉需要回滾但是沒回滾的場景攒霹。
總結(jié)
會(huì)關(guān)注dble xa事務(wù)邏輯怯疤,還是和工作中被分配到測試dble xa事務(wù)代碼有關(guān)系。
我按照自己的思想做了一個(gè)帶錯(cuò)誤注入的server內(nèi)部xa流程自動(dòng)化破壞性測試工具催束,測試出了一些問題集峦,給dble提了issue和PR。制作測試工具泣崩,修復(fù)問題還是占了很多時(shí)間的少梁,所以上面的分析如果有紕漏,請不吝賜教矫付。