DBLE XA事务源码分析

XA 本身

关于xa本身不会过多介绍(因为我也是看别人写的啊),可以两个方向去搜索

  1. 【官方】xa规范官方文档和MySQL xa官方文档
  2. 【民间】别人已经学习过记录的博客
    可以阅读 https://www.jianshu.com/p/7003d58ea182 ,我就用这个文章学习的

拓扑结构

借用https://www.jianshu.com/p/7003d58ea182里的图片

image.png

在DBLE这样的分库分表中间里,拓扑结构其实是这样的。

image.png

DBLE Server进程是AP,TM是作为代码逻辑模块内嵌在DBLE Server进程内部的。DBLE Server使用MySQL xa事务sql语法去定义事务边界。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用于确定一个分支事务的边界
  • XA PREPARE 和XA COMMIT,XA ROLLBACK用来做2PC
  • XA RECOVER是管控命令,用来查看一阶段prepare过但是还没二阶段commit或者rollback的分支事务

XA事务状态变迁图

借用https://www.jianshu.com/p/7003d58ea182里的XA事务状态变迁

image.png

这个图需要注意的地方是
(1)如果你xa start开启一个全局事务以后,你不走到两个终点,你是没法继续开启下一个xa事务的。即使你xa start后什么DML也没执行,你也需要按照xa end, xa rollback的流程这样结束事务,否则XAER_RMFAIL: The command cannot be executed when global transaction is in the %s state这样的报错就会来敲门。
(2)当分布式事务里只有一个RM,即分布式事务退化成本地事务了,xa commit one phase 时会将prepare和commit 一起完成。 DBLE里面并没有使用这种优化!

再次借用https://www.jianshu.com/p/7003d58ea182里的图,这样只有四种到最终状态的路径。

image.png

关注xa prepare

在xa prepare执行成功之前,如果你关闭到MySQL的链接,事务会被MySQL自动回滚的,不会留下任何副作用。

但是在xa prepare执行成功之后,你必须决定commit还是rollback,并且按照上图的流程图走到结束状态。因为xa prepare执行成功之后,事务信息将被持久化,不管连接断开还是MySQL Server重启,事务都将被重新恢复(通过information_schema.innodb_trx你可以查询到),这些事务占有的锁都会阻止其他会产生锁冲突的事务继续执行。这是和MySQL本地事务不同的地方。通过xa recover可以查询到这些xa prepare过的事务。

dble官方文档的图片中也说到这些

image.png

binlog

binlog会记录xa事务的日志


image.png

个人发现

  1. 只有xa prepare执行后才会记录binlog,但是会记录到此为止的所有语句。
  2. 没实际修改到数据的DML不会记录binlog

DBLE XA实现分析

https://actiontech.github.io/dble-docs-cn/2.Function/2.05_distribute_transaction.html
可以先阅读dble xa事务的文档,建立一些术语概念

分库分表中间件用XA事务 解决跨库事务的思路

分库分表中间件对用户来说就像一个MySQL,分库分表中间件自己面对多个MySQL。
用户发出的逻辑sql,会被中间件解析成多条到不同MySQL的物理sql。
在中间件不使用任何分布式事务解决方案的时候,只是在多个MySQL上执行本地事务,和用户自己连接到MySQL上执行本地事务一致。这种最大努力尝试提交的方式是无法保证数据一致性的。

无论中间件使用xa事务还是使用本地事务,都是分库分表中间件内部自己使用的事务,业务代码访问分库分表中间件仍然使用的是普通本地事务,这也是分库分表中间件一直在宣传的(有限)透明, 对业务代码侵入低。

这样去想,就知道分库分表中间件在用xa解决跨库事务的时候要做到的流程

业务侧连接到中间件执行本地事务的一般步骤

  • 显式事务
start transaction / begin
DML
commit/rollback
  • 隐式事务
set autocommit=0;
DML
commit/rollback
  • autocommit开启时的单条的跨库sql

autocommit为true的单条sql原子事务

delete from xa_test where id in (1,2,3);

分库分表中间件接受到用户本地事务命令的反应

以显式事务为例

  1. start transaction / begin
    中间件设置前端连接的事务开始标志
  2. DML
    逻辑DML可能翻译成一条或多条物理DML
    使用相关物理分库对应的后端连接,中间件执行xa start和物理DML语句
  3. commit/rollback
    使用相关物理分库对应的后端连接,执行xa end,xa preapre, xa commit或者xa rollback, 结束后清理前段连接的事务开始标志

源码结构

com.actiontech.dble.backend.mysql.nio.handler.transaction.xa

xa事务控制语句执行后的回调,负责推动流程进行
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事务状态定义,注意里面的定义既包括全局事务的状态,也包括分支事务的状态定义

  • ing结尾的就是sql执行刚开始
  • (start,end,prepare,commit,rollback)ed结尾的是明确执行成功(或者失败了也没副作用的场景)
  • ed结尾有UNCONNECT后缀的是网络错误导致执行结果未知的场景
  • FAILED就是执行失败状态的,按照前面说的,必须要重试变成commited或者rollbacked

com.actiontech.dble.backend.mysql.xa.recovery

事务日志,存本地还是zk,server宕机后启动的事务中断恢复逻辑

客户端连接DBLE Server代码示例

下面只是显式事务的例子,正常使用jdbc你应该不会这样写。
DBLE需要用户额外执行一个set xa=1,来告诉中间件接下来的sql,中间件需要使用xa事务。如果不执行set xa=1, dble将在每个MySQL上使用本地事务。
也可以通过在jdbc url里面增加参数(jdbc:mysql://127.0.0.1:8066?sessionVariables=xa=1)来避免显式执行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();
}

详细执行流程

先上图来些印象,这个图可能不是太正规,我只是按照便于我理解的思路去画的。
主要画的是事务状态的流转

  • 全局事务状态 com.actiontech.dble.server.NonBlockingSession#xaState
  • 分支事务状态 com.actiontech.dble.backend.mysql.nio.MySQLConnection#xaStatus

显式事务 commit成功

image.png

显式事务 rollback成功

image.png

代码流程分析

1. set xa=1;

com.actiontech.dble.server.handler.SetHandler#handleSingleXA
如果之前xa事务开启标志没打开的话,这里打开会重新生成一次xid。
所以如果打开以后,只要不关闭再打开,dble的一个前端连接,一直使用的是相同的xid.

1.1 xid的生成

在MySQL官方文档中,关于xid的组成也有类似的说明:
xid: gtrid [, bqual [, formatID ]]

其中,bqual、formatID是可选的。解释如下:
gtrid : 是一个全局事务标识符(global transaction identifier),
bqual:是一个分支限定符(branch qualifier),如果没有提供bqual,那么默认值为空字符串''。
formatID:是一个数字,用于标记gtrid和bqual值的格式,这是一个无符号整数(unsigned integer),也就是说,最小为0。如果没有提供formatID,那么其默认值为1。

com.actiontech.dble.DbleServer#genXaTxId
DBLE是只使用了gtrid的部分,感觉有些不规范。但是因为xid传到了MySQL的时候,MySQL只是个分支事务,所以每个分支事务的xid不同,只要TM自己知道哪些xid是一个全局事务里的就行了。按照规范来的好处我觉得是方便人工去判断某个分支事务是哪个全局事务,而不需要到TM的事务日志里去查询。

zk集群模式下的全局事务xid是

'Dble_Server.{myid}.{进程内原子自增数}'

实际在执行xa start的时候使用的分支事务xid是会在加上物理库的名称

'Dble_Server.{myid}.{进程内原子自增数}.{schema}'

保证了每个前端连接的xid在DBLE Server集群内是唯一的。

2. begin/start transaction

com.actiontech.dble.server.handler.BeginHandler#handle
begin/start transaction是在单次事务里关闭autocommit模式,这意味着,在本地事务执行结束后,事务前的autocommit还要被恢复回来。后面的细节。

3. DML

  • com.actiontech.dble.backend.mysql.nio.MySQLConnection#synAndDoExecute
    回调函数是com.actiontech.dble.backend.mysql.nio.handler.SingleNodeHandler
    对应用户本地事务中某条不跨库的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
    回调函数是com.actiontech.dble.backend.mysql.nio.handler.MultiNodeQueryHandler
    用户本地事务中某条跨库的sql
set xa=1;
begin;
insert into xa_test (id) values (1,2);
commit;

这两个方法需要关注com.actiontech.dble.backend.mysql.nio.MySQLConnection.StatusSync这个类。
除了DML本身,dble会把事务控制语句,和set autocommit这些语句拼在DML前面,然后在一次请求里发送给MySQL。

XA START 'Dble_Server.server_01.791.db2';INSERT INTO xa_test
VALUES (1);

需要注意,这样拼接多个SQL一起发过去的时候,如果中间某个sql出错,后续sql的响应是不会收到的。MultiNodeQueryHandler#errorResponse的处理是不当的,会导致程会话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

回调函数需要关注分布式系统调用的三态

  • 事务控制语句执行结果未知
    ResponseHandler#connectionError
    ResponseHandler#connectionClose

  • 事务控制语句执行明确失败
    ResponseHandler#errorResponse

  • 事务控制语句执行明确成功
    ResponseHandler#okResponse

这里我并不想仔细分析,你可以看我上面的事务状态流程图,也可以理解。自己调试一遍才对状态流转有个感受。

4.1 handler的复用

一般来说dble里面回调函数是每次执行sql时创建一个对象,执行完就释放了。回调函数只会被使用一次。
但是XACommitNodesHandler和XARollbackNodesHandler是被多次复用的,因为在用户执行commit或者rollback后,dble需要执行多次sql,例如xa end,xa prepare,xa commit。

handler的复用需要仔细的清除上次sql执行遗留的状态,否则可能会影响下次xa事务的执行,详见我提的issue https://github.com/actiontech/dble/issues/1156 ,PS: issue提到的问题是我自己解决前面问题后碰到的,正常代码可能没有场景复现,但是这里确实是一个代码逻辑问题。

4.2 事务控制语句所有节点全部发送完才开始处理响应

对于某个流程控制语句,例如xa end xid这种,必须所有MySQL都发送完成后,回调函数才能处理响应。
发送时候是遍历所有分支事务去发送语句的,但是先收到响应的人必须等所有人都发送完成后,才能开始处理响应。
通过在XACommitNodesHandler#waitUntilSendFinish和XARollbackNodesHandler#waitUntilSendFinish上面阻塞实现的。

我在测试某个【从DBLE迁移XA功能】的MyCAT分支代码时发现, 如果只迁移DBLE XA部分本身的逻辑,DBLE的发送细节会造成一些危险。在遍历发送XA控制语句结束前,如果后端连接出现网络断开,io.mycat.backend.mysql.nio.MySQLConnection.close会在发送线程里触发回调函数。因为所有XA事务控制语句 全部发送完毕才会调用Condition#signalAll,所以发送线程卡死在回调函数的waitUntilSendFinish方法上。

DBLE里面关闭连接是异步调用回调函数的(com.actiontech.dble.backend.mysql.nio.MySQLConnection#closeResponseHandler),所以没有MyCAT里面的这个卡死问题。

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 出错处理

(1)丢弃后端连接
前面说过xa prepare执行成功之前,丢弃后端连接都可以让MySQL自动回滚事务,下面回调函数方法大多这样处理的。
MultiNodeQueryHandler#errorResponse
XACommitNodesHandler#errorResponse
XARollbackNodesHandler#errorResponse

但是个人感觉MultiNodeQueryHandler#errorResponse里面,如果DML执行报错的话,是否不用丢弃后端连接?执行XA END和XA ROLLBACK, 可以保留一个后端连接。
(2)重新获取后端连接
xa prepare以及其后阶段的事务控制语句,执行时出现网络断开的时候,后端连接已经不可用了,但是事务语句可能会执行成功,只是DBLE Server没有接收到响应,所以必须重新获取一个后端连接去做某些操作,rollback或者明确事务结果。
这里刷新调用的是com.actiontech.dble.server.NonBlockingSession#freshConn

需要注意的是,不只是老连接关闭的场景下回走到NonBlockingSession#freshConn,老的连接没关闭的时候也会使用NonBlockingSession#freshConn。而NonBlockingSession#freshConn会用新的链接替换老连接在com.actiontech.dble.server.NonBlockingSession#target里的键值对,session本身只会清理NonBlockingSession#target里的连接,如果freshConn的时候不释放老的链接,可能会造成连接泄露。从单独使用NonBlockingSession#freshConn替换连接的语义上来说,freshConn方法可以直接把老的链接给释放掉。详见https://github.com/actiontech/dble/issues/1158

(3)xa prepare执行成功后 commit或者rollback的重试
前面说过xa prepare没执行成功前,关闭后端连接是个快刀斩乱麻的解决方式。
但当某个分支事务的xa prepare执行成功后,则必须成功执行xa rollback或者xa commit。
DBLE这里处理策略是,在一定次数内去重试xa commit或者xa rollback,如果成功了,用户不会有任何察觉,用户只是觉得commit或者rollback有点慢。
但是如果一定次数全部都没重试成功,那么dble会断开前端连接。这里是正确的处理方式,当用户通过jdbc执行commit或者rollback发生连接断开的时候,MySQL JDBC驱动会抛com.mysql.jdbc.SQLError#SQL_STATE_TRANSACTION_RESOLUTION_UNKNOWN差异,告知用户事务结果不明 Communications link failure during commit()/rollback(). Transaction resolution unknown.
之后DBLE Server会后台无限重试commit或者rollback,确保事务一定成功commit或者rollback.

在manager端口通过show @@session.xa命令,可以查询到在进行提交和回滚的事务。
com.actiontech.dble.manager.response.ShowXASession#execute

理解全局事务状态和分支事务状态的关系

com.actiontech.dble.backend.mysql.xa.TxState里事务状态,有的是中间失败状态,有的是终点成功状态。

在dble代码里
只要有一个分支事务失败的时候,分支事务的失败状态也会设置到全局事务的状态上去。
所有分支事务都被设置成某个成功状态的时候,全局事务的状态才有可能会被设置成分支事务的成功的状态。

这样就知道NonBlockingSession#releaseExcept方法的逻辑了。

例如XACommitNodesHandler#cleanAndFeedback里面, 全局事务是个失败的状态,那么必然是从某个分支事务的失败状态来的,如果还有分支事务状态和全局事务状态一样,那么还有人没重试成功。

} else if (session.getXaState() == TxState.TX_COMMIT_FAILED_STATE) {
    // 还有分支事务和全局事务的失败状态一致的么
    MySQLConnection errConn = session.releaseExcept(TxState.TX_COMMIT_FAILED_STATE);
    if (errConn != null) {
        // 还有人没有执行成功,继续重试
        ......
    } else {
        // 大家都成功了,收工了
        ......
    }
}

不按套路出牌的用户如何处理

正常情况我们用jdbc事务的时候,如果DML执行出错,抛出异常,我们会在catch语句里进行rollback。
但是如果用户不仅不rollback,还去commit或者执行其他语句怎么办?

com.actiontech.dble.server.ServerConnection#txInterrupted 前端连接有个flag, 如果执行失败,需要回滚的时候,这个标志会被打开,下面再进行任何非rollback的语句都会将com.actiontech.dble.server.ServerConnection#txInterruptMsg的报错信息返回给用户。

image.png

image.png

显式XA事务 部分场景下的commit失败时会自动回滚

正常来说,XAAutoCommitNodesHandler和XAAutoRollbackNodesHandler只会被用在:
在autocommit为true,启用xa事务的前端连接(set xa=1)里,用户执行了一条跨库的sql,因为autocommit开启的时候,单条sql就是一个原子事务,所以这条sql执行过程里dble会走xa流程,进行自动提交或者自动回滚。

但是代码里的有的地方,在显式事务的时候,如果用户执行commit, DBLE Server执行xa prepare出现网络断开的时候,dble会自动回滚事务。

代码在com.actiontech.dble.backend.mysql.nio.handler.transaction.xa.XACommitNodesHandler#nextParse,非TX_PREPARE_UNCONNECT_STATE的状态,由客户之后的rollback进行回滚。
TX_PREPARE_UNCONNECT_STATE的会自动进行回滚,之后客户执行rollback的时候,其实事务已经执行结束了,所以rollback只是个过场。

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();
    }
}

这里的自动回滚我一开始没有理解,加上https://github.com/actiontech/dble/issues/1170里面自动回滚没有把真实出错原因返回给用户,而是直接返回OK,所以我当时认为应该让客户自己rollback。

后来对比MySQL本地事务,MySQL什么时候会commit失败,MySQL commit失败的时候会不会自动进行回滚?如果sql全部执行成功,我百度不到MySQL commit会失败的场景。但是在中间件xa事务的时候,commit由很多步骤组成,所以commit会出现失败场景,在commit失败的时候,中间件进行回滚,这样看来是可以接受的。

事务执行过程中,前端连接被关闭

如果用户在执行xa事务的过程中,用kill connection id去强杀前端连接,DBLE Server应该如何处理?

如果会话执行过程中,用户和DBLE Server的链接发生断开如何处理? (例如jdbc statement timeout, jdbc connection 的socket timeout等,他们统统都是下面这样处理的。)

强杀和前他原因导致的前端连接关闭 分别在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();
    }
}

第一个判断逻辑
(1)session.getSource().isTxStart() // XA事务开启,显式begin/start transaction或者autocommit=false执行DML过
(2)session.getXaState() != null && session.getXaState() != TxState.TX_INITIALIZE_STATE //执行过DML
(3)!session.cancelableStatusSet(NonBlockingSession.CANCEL_STATUS_CANCELING)

注意上面的第(3)个条件,可以看下com.actiontech.dble.server.NonBlockingSession#cancelableStatusSet在什么时候会被调用

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;
    }
}

这里达到的效果是是,只有全局事务在commit或者rollback过程中还没进入到xa end执行之后的阶段时(xa prepare,xa commit,xa rollback这些),才会在关闭前端连接的同时,也关闭后端连接。否则只会关闭前端连接,后端连接继续执行事务流程。

事务日志和事务恢复

事务日志

DBLE Server侧的事务日志非常重要,因为每个MySQL只是分支事务参与者,在DBLE Server宕机后,仅从MySQL侧是无法知道全局事务在二阶段时是决议提交还是决议回滚的。

事务日志主要记录了全局事务(CoordinatorLogEntry)和其所包含的分支事务(ParticipantLogEntry)的信息(这样xid就算随便起也没事,这里TM知道对应关系)
主要关注下事务状态

  • 事务日志中的全局事务状态 com.actiontech.dble.backend.mysql.xa.CoordinatorLogEntry#setTxState
  • 事务日志中的分支事务状态 com.actiontech.dble.backend.mysql.xa.ParticipantLogEntry#setTxStat

更新全局事务信息
(1) com.actiontech.dble.backend.mysql.xa.XAStateLog#saveXARecoveryLog(java.lang.String, com.actiontech.dble.backend.mysql.xa.TxState)

更新分支事务信息
(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)

刷盘时机

只有更新全局事务信息的时候才会刷盘,但并不是每次更新都会刷盘,大部分时候都只是修改内存里的值。
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;
}

这样的刷盘时机是可能导致一些问题,详见我提的issue https://github.com/actiontech/dble/issues/1192,主要是内存里的事务状态还没被刷到磁盘的时候server宕机,server启动恢复的时候需要顺着流程往前多判断一些事务状态,否则会漏恢复prepare过的事务,造成事务泄露。

事务恢复

server如果宕机了,DBLE Server在重启的时候会进行恢复。
PS: 其实觉得如果有server宕机了,在zk集群的时候,其他存活的节点能否接管进行事务恢复?

什么样的中断事务需要恢复?
本质上server宕机时,在显式/隐式 提交/回滚的时候,事务状态都是未知的,这样server这里只要根据事务流程进行到的阶段,除非数据一致性必须要commit的场景才重试xa commit全局事务(事务原子性),否则就进行全局事务回滚(释放锁之类的)。

准确说,任何一个分支事务只要xa prepare可能执行成功了,但是还没有明确成功执行过xa commit或者xa rollback的话,就要在启动的时候进行恢复。
这点从com.actiontech.dble.DbleServer#performXARecoveryLog里面可以看出来,先判断全局事务状态,决定全局事务是否需要提交或者回滚。这里的状态全部都是xa prepare可能执行成功过的。

// 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现在的判断逻辑是宁滥毋缺的,可能不需要回滚的场景,也会回滚的,但是绝对不会漏掉需要回滚但是没回滚的场景。

总结

会关注dble xa事务逻辑,还是和工作中被分配到测试dble xa事务代码有关系。
我按照自己的思想做了一个带错误注入的server内部xa流程自动化破坏性测试工具,测试出了一些问题,给dble提了issue和PR。制作测试工具,修复问题还是占了很多时间的,所以上面的分析如果有纰漏,请不吝赐教。

参考文章

  • MySQL XA 介绍
  • https://actiontech.github.io/dble-docs-cn/2.Function/2.05_distribute_transaction.html
  • https://dev.mysql.com/doc/refman/5.7/en/xa.html

你可能感兴趣的:(DBLE XA事务源码分析)