DBLE XA事務(wù)源碼分析

XA 本身

關(guān)于xa本身不會(huì)過多介紹(因?yàn)槲乙彩强磩e人寫的啊),可以兩個(gè)方向去搜索

  1. 【官方】xa規(guī)范官方文檔和MySQL xa官方文檔
  2. 【民間】別人已經(jīng)學(xué)習(xí)過記錄的博客
    可以閱讀 http://www.reibang.com/p/7003d58ea182 刊侯,我就用這個(gè)文章學(xué)習(xí)的

拓?fù)浣Y(jié)構(gòu)

借用http://www.reibang.com/p/7003d58ea182里的圖片

image.png

在DBLE這樣的分庫分表中間里紧武,拓?fù)浣Y(jié)構(gòu)其實(shí)是這樣的函匕。

image.png

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)變遷

image.png

這個(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)的路徑。

image.png

關(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官方文檔的圖片中也說到這些

image.png

binlog

binlog會(huì)記錄xa事務(wù)的日志


image.png

個(gè)人發(fā)現(xiàn)

  1. 只有xa prepare執(zhí)行后才會(huì)記錄binlog,但是會(huì)記錄到此為止的所有語句副硅。
  2. 沒實(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ù)為例

  1. start transaction / begin
    中間件設(shè)置前端連接的事務(wù)開始標(biāo)志
  2. DML
    邏輯DML可能翻譯成一條或多條物理DML
    使用相關(guān)物理分庫對(duì)應(yīng)的后端連接壁公,中間件執(zhí)行xa start和物理DML語句
  3. 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成功

image.png

顯式事務(wù) rollback成功

image.png

代碼流程分析

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è)卡死問題朦前。

image.png

卡住的線程堆棧如下

"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ò)信息返回給用戶。

image.png

image.png

顯式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)用

image.png

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í)間的少梁,所以上面的分析如果有紕漏,請不吝賜教矫付。

參考文章

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末凯沪,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子买优,更是在濱河造成了極大的恐慌妨马,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,290評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件杀赢,死亡現(xiàn)場離奇詭異烘跺,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)脂崔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門滤淳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人砌左,你說我怎么就攤上這事脖咐。” “怎么了汇歹?”我有些...
    開封第一講書人閱讀 156,872評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵皆怕,是天一觀的道長讨惩。 經(jīng)常有香客問我杠巡,道長床估,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,415評(píng)論 1 283
  • 正文 為了忘掉前任痰哨,我火速辦了婚禮胶果,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘斤斧。我一直安慰自己早抠,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,453評(píng)論 6 385
  • 文/花漫 我一把揭開白布折欠。 她就那樣靜靜地躺著贝或,像睡著了一般吼过。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上咪奖,一...
    開封第一講書人閱讀 49,784評(píng)論 1 290
  • 那天盗忱,我揣著相機(jī)與錄音,去河邊找鬼羊赵。 笑死趟佃,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的昧捷。 我是一名探鬼主播闲昭,決...
    沈念sama閱讀 38,927評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼靡挥!你這毒婦竟也來了序矩?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,691評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤跋破,失蹤者是張志新(化名)和其女友劉穎簸淀,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體毒返,經(jīng)...
    沈念sama閱讀 44,137評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡租幕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,472評(píng)論 2 326
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了拧簸。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片劲绪。...
    茶點(diǎn)故事閱讀 38,622評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖盆赤,靈堂內(nèi)的尸體忽然破棺而出贾富,到底是詐尸還是另有隱情,我是刑警寧澤弟劲,帶...
    沈念sama閱讀 34,289評(píng)論 4 329
  • 正文 年R本政府宣布祷安,位于F島的核電站姥芥,受9級(jí)特大地震影響兔乞,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜凉唐,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,887評(píng)論 3 312
  • 文/蒙蒙 一庸追、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧台囱,春花似錦淡溯、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽米间。三九已至,卻和暖如春膘侮,著一層夾襖步出監(jiān)牢的瞬間屈糊,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來泰國打工琼了, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留逻锐,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,316評(píng)論 2 360
  • 正文 我出身青樓雕薪,卻偏偏與公主長得像昧诱,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子所袁,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,490評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容

  • MySQL分布式事務(wù)介紹 InnoDB存儲(chǔ)引擎提供了對(duì)XA事務(wù)的支持盏档,并通過XA事務(wù)來支持分布式事務(wù)的實(shí)現(xiàn)。分布式...
    張偉科閱讀 9,111評(píng)論 0 10
  • Vitess之前燥爷,先復(fù)習(xí)一下事務(wù)的四個(gè)基本特性 原子性:一個(gè)事務(wù)對(duì)狀態(tài)的改變是原子的妆丘,要么都發(fā)生,要么都不發(fā)生局劲,這...
    許海華閱讀 2,938評(píng)論 3 10
  • 事務(wù)(Transaction)是數(shù)據(jù)庫區(qū)別于文件系統(tǒng)的重要特性之一勺拣。 在文件系統(tǒng)中, 如果正在寫文件鱼填, 但是操作系...
    好好學(xué)習(xí)Sun閱讀 1,004評(píng)論 0 5
  • Sort List Sort a linked list in O(n log n) time using con...
    金發(fā)萌音閱讀 417評(píng)論 0 2
  • 應(yīng)對(duì)情緒的基礎(chǔ)是認(rèn)知情緒药有。 認(rèn)知情緒是感知并解釋情緒信息的能力。 認(rèn)知情緒既針對(duì)自己苹丸,又針對(duì)他人愤惰,就是既要認(rèn)知自己...
    金石明鏡閱讀 462評(píng)論 0 1