深入理解分布式事务:原理与实战
上QQ阅读APP看书,第一时间看更新

2.5 MySQL中的XA事务

MySQL 5.0.3版本开始支持XA分布式事务,并且只有InnoDB存储引擎支持XA事务,MySQL Connector/J 5.0.0版本之后开始提供对XA事务的支持。本节对MySQL中的XA事务进行简单的介绍。

2.5.1 XA事务的基本原理

XA事务支持不同数据库之间实现分布式事务。这里的不同数据库,可以是不同的MySQL实例,也可以是不同的数据库类型,比如MySQL数据库和Oracle数据库。

XA事务本质上是一种基于两阶段提交的分布式事务,分布式事务可以简单理解为多个数据库事务共同完成一个原子性的事务操作。参与操作的多个事务要么全部提交成功,要么全部提交失败。在使用XA分布式事务时,InnoDB存储引擎的事务隔离级别需要设置为串行化。

XA事务由一个事务管理器(Transaction Manager)、一个或者多个资源管理器(Resource Manager)和一个应用程序(Application Program)组成,组成模型如图2-13所示。

图2-13 XA事务模型

1)事务管理器:主要对参与全局事务的各个分支事务进行协调,并与资源管理器进行通信。

2)资源管理器:主要提供对对事务资源的访问能力。实际上,一个数据库就可以看作一个资源管理器。

3)应用程序:主要用来明确全局事务和各个分支事务,指定全局事务中的各个操作。

因为XA事务是基于两阶段提交的分布式事务,所以XA事务也被拆分为Prepare阶段和Commit阶段。

在Prepare阶段,事务管理器向资源管理器发送准备指令,资源管理器接收到指令后,执行数据的修改操作并记录相关的日志信息,然后向事务管理器返回可以提交或者不可以提交的结果信息。

在Commit阶段,事务管理器接收所有资源管理器返回的结果信息,如果某一个或多个资源管理器向事务管理器返回的结果信息为不可以提交,或者超时,则事务管理器向所有的资源管理器发送回滚指令。如果事务管理器收到的所有资源管理器返回的结果信息为可以提交,则事务管理器向所有的资源管理器发送提交事务的指令。

2.5.2 MySQL XA事务语法

在MySQL命令行输入如下命令可以查看存储引擎是否支持XA事务。


mysql> show engines \G
*************************** 1. row ***************************
      Engine: InnoDB
     Support: DEFAULT
     Comment: Supports transactions, row-level locking, and foreign keys
Transactions: YES
          XA: YES
  Savepoints: YES
*************************** 2. row ***************************
      Engine: MRG_MYISAM
     Support: YES
     Comment: Collection of identical MyISAM tables
Transactions: NO
          XA: NO
  Savepoints: NO
*************************** 3. row ***************************
      Engine: MEMORY
     Support: YES
     Comment: Hash based, stored in memory, useful for temporary tables
Transactions: NO
          XA: NO
  Savepoints: NO
*************************** 4. row ***************************
      Engine: BLACKHOLE
     Support: YES
     Comment: /dev/null storage engine (anything you write to it disappears)
Transactions: NO
          XA: NO
  Savepoints: NO
*************************** 5. row ***************************
      Engine: MyISAM
     Support: YES
     Comment: MyISAM storage engine
Transactions: NO
          XA: NO
  Savepoints: NO
*************************** 6. row ***************************
      Engine: CSV
     Support: YES
     Comment: CSV storage engine
Transactions: NO
          XA: NO
  Savepoints: NO
*************************** 7. row ***************************
      Engine: ARCHIVE
     Support: YES
     Comment: Archive storage engine
Transactions: NO
          XA: NO
  Savepoints: NO
*************************** 8. row ***************************
      Engine: PERFORMANCE_SCHEMA
     Support: YES
     Comment: Performance Schema
Transactions: NO
          XA: NO
  Savepoints: NO
*************************** 9. row ***************************
      Engine: FEDERATED
     Support: NO
     Comment: Federated MySQL storage engine
Transactions: NULL
          XA: NULL
  Savepoints: NULL
9 rows in set (0.00 sec)

从输出的结果信息来看,只有InnoDB存储引擎支持事务、XA事务和事务保存点。

MySQL XA事务的基本语法如下所示。

1)开启XA事务,如果使用的是XA START命令而不是XA BEGIN命令,则不支持[JOIN|RESUME],xid是一个唯一值,表示事务分支标识符,语法如下。


XA {START|BEGIN} xid [JOIN|RESUME]

2)结束一个XA事务,不支持[SUSPEND[FOR MIGRATE]],语法如下。


XA END xid [SUSPEND [FOR MIGRATE]]

3)准备提交XA事务。


XA PREPARE xid

4)提交XA事务,如果使用了ONE PHASE命令,表示使用一阶段提交。在两阶段提交协议中,如果只有一个资源管理器参与操作,则可以优化为一阶段提交。


XA COMMIT xid [ONE PHASE]

5)回滚XA事务。


XA ROLLBACK xid

6)列出所有处于准备阶段的XA事务。


XA RECOVER [CONVERT XID]

关于MySQL XA事务的更多语法,读者可以参考MySQL官方文档,地址为https://dev.mysql.com/doc/refman/8.0/en/xa-states.html。如果使用的是MySQL 5.7版本,则可以到https://dev.mysql.com/doc/refman/5.7/en/xa-states.html进行查阅,笔者不再赘述。

下面是MySQL官方文档中对于XA事务的一个简单示例,演示了MySQL作为全局事务中的一个事务分支,将一行记录插入一个表。


mysql> XA START 'xatest';
Query OK, 0 rows affected (0.00 sec)

mysql> INSERT INTO mytable (i) VALUES(10);
Query OK, 1 row affected (0.04 sec)

mysql> XA END 'xatest';
Query OK, 0 rows affected (0.00 sec)

mysql> XA PREPARE 'xatest';
Query OK, 0 rows affected (0.00 sec)

mysql> XA COMMIT 'xatest';
Query OK, 0 rows affected (0.00 sec)

MySQL XA事务使用XID标识分布式事务,xid主要由以下几部分组成。


xid: gtrid[, bqual [, formatID ]]

1)gtrid:必须,为字符串,表示全局事务标识符。

2)bqual:可选,为字符串,默认是空串,表示分支限定符。

3)formatID:可选,默认值为1,用于标识gtrid和bqual值使用的格式。

2.5.3 JDBC操作MySQL XA事务

这里单独使用一个小节介绍如何使用JDBC操作MySQL XA事务。MySQL Connector/J 5.0.0版本开始支持XA事务,也就是说,从Connector/J 5.0.0版本开始提供了Java版本XA接口的实现。基于此,可以直接通过Java代码来执行MySQL的XA事务。

JDBC操作MySQL XA事务的完整源码如下所示。


import com.mysql.jdbc.jdbc2.optional.MysqlXAConnection;
import com.mysql.jdbc.jdbc2.optional.MysqlXid;
import javax.sql.XAConnection;
import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class MysqlXAConnectionTest {
    public static void main(String[] args) throws SQLException {
        //打印XA日志
        boolean writeLog = true;
        // 获得资源管理器操作接口实例RM1
        Connection conn1 = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "binghe", "binghe123");
        //配置打印XA日志
        XAConnection xaConn1 = new MysqlXAConnection((com.mysql.jdbc.Connection) conn1, writeLog);
        XAResource rm1 = xaConn1.getXAResource();
        // 获得资源管理器操作接口实例RM2
        Connection conn2 = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "binghe","binghe123");
        //配置打印XA日志
        XAConnection xaConn2 = new MysqlXAConnection((com.mysql.jdbc.Connection) conn2, writeLog);
        XAResource rm2 = xaConn2.getXAResource();
        // 应用程序请求事务管理器执行一个分布式事务,事务管理器生成全局事务id
        byte[] gtrid = "binghe123".getBytes();
        int formatId = 1;
            Xid xid1=null;
            Xid xid2=null;
        try {
            // ==============分别执行RM1和RM2上的事务分支====================
            // 事务管理器生成rm1上的事务分支id
            byte[] bqual1 = "binghe001".getBytes();
            xid1 = new MysqlXid(gtrid, bqual1, formatId);
            // 执行rm1上的事务分支
            rm1.start(xid1, XAResource.TMNOFLAGS);
            PreparedStatement ps1 = conn1.prepareStatement("INSERT into xa_test(name) VALUES ('binghe')");
            ps1.execute();
            rm1.end(xid1, XAResource.TMSUCCESS);
            // 事务管理器生成rm2上的事务分支id
            byte[] bqual2 = "binghe002".getBytes();
            xid2 = new MysqlXid(gtrid, bqual2, formatId);
            // 执行rm2上的事务分支
            rm2.start(xid2, XAResource.TMNOFLAGS);
            PreparedStatement ps2 = conn2.prepareStatement("INSERT into xa_test(name) VALUES ('binghe')");
            ps2.execute();
            rm2.end(xid2, XAResource.TMSUCCESS);
            // ===================两阶段提交================================
            // 第一阶段:通知所有的资源管理器准备提交事务分支
            int rm1_prepare = rm1.prepare(xid1);
            int rm2_prepare = rm2.prepare(xid2);
            // 第二阶段:提交所有事务分支
            boolean onePhase = false; 
            //所有事务分支都进入准备状态,提交所有事务分支
            if (rm1_prepare == XAResource.XA_OK
                    && rm2_prepare == XAResource.XA_OK ) { 
                rm1.commit(xid1, onePhase);
                rm2.commit(xid2, onePhase);
            } else {   //如果有事务分支没有进入准备状态,则回滚所有的分支事务
                rm1.rollback(xid1);
                rm2.rollback(xid2);
            }
        } catch (XAException e) {
            // 如果出现异常,也要进行回滚
                rm1.rollback(xid1);
                rm2.rollback(xid2);
            e.printStackTrace();
        }
    }
}

可以看到,直接使用JDBC操作MySQL的XA事务还是挺烦琐的,不过在实际的工作中,很少使用JDBC直接操作MySQL的XA事务,大部分时间会使用第三方框架或者容器来操作XA事务,能够大大提高开发的效率。

在某种程度上,MySQL XA事务可分为内部XA事务和外部XA事务。外部XA事务属于分布式事务的一种实现方式,而内部XA事务则表示MySQL使用了InnoDB作为存储引擎,并且开启了BinLog,为了保证BinLog与Redo Log的一致性,MySQL内部使用了XA事务。