分类
外汇交易平台比较

使用 XA 交易時的指導方針和限制

MySQL 从5.0.3开始支持XA分布式事务,且只有InnoDB存储引擎支持。入下图:

基于 XA 事务协议,用代码实现一个二阶段分布式事务

MySQL 从5.0.3开始支持XA分布式事务,且只有InnoDB存储引擎支持。入下图:

在 MySQL数据库官网有一个模块专门讲 XA 事务,具体可以查看:

其他的我就不说了,这里我提一下 XA 事务状态,一个完整的事务流程如下:

  • 1.使用 XA START 来启动一个 XA 事务,并把它置于 ACTIVE 状态。
  • 2.对于一个 ACTIVE 状态的 XA 事务,我们可以执行构成事务的 SQL 语句,然后发布一个 XA END 语句。XA END 把事务放入 IDLE 状态。
  • 3.对于一个 IDLE 状态XA事务,可以执行一个 XA PREPARE 语句或一个XA COMMIT…ONE PHASE 语句:
    • XA PREPARE 把事务放入 PREPARED 状态。在此点上的 XA RECOVER 语句将在其输出中包括事务的 xid 值,因为 XA RECOVER 会列出处于 PREPARED 状态的所有 XA 事务。
    • XA COMMIT…ONE PHASE 用于预备和提交事务。xid 值将不会被 XA RECOVER 列出,因为事务终止。

    总结一下,XA 事务,通过 Start 启动一个 XA 事务,并且被置为 Active 状态,处在 active 状态的事务可以执行 SQL 语句,通过 END 方法将 XA 事务置为 IDLE 状态。处于 IDLE 状态可以执行 PREPARE 操作或者 COMMIT…ONE PHASE 操作,也就是二阶段提交中的第一阶段,PREPARED 状态的 XA事务的时候就可以 Commit 或者 RollBack,也就是二阶段提交的第二阶段

    可能你注意到了上面有一个 XID 值,简单的讲一下,MySQL 中使用xid来作为一个事务分支的标识符。关于 xid 在 XA 使用 XA 交易時的指導方針和限制 使用 XA 交易時的指導方針和限制 规范中有定义,XA规范定义了一个xid有4个部分组成:

    • gtrid:全局事务标识符(global transaction identifier),最大不能超过64字节。
    • bqual:分支限定符(branch qualifier),最大不能超过64字节。
    • data:xid的值,其是 使用 XA 交易時的指導方針和限制 gtrid和bqual拼接后的内容。
    • formatId:formatId的作用就是记录gtrid、bqual的格式,类似于memcached中flags字段的作用。

    好了,关于 XA 事务就 BB 这么多了,接下来,我们通过一个实例,来实现一把基于 XA 事务协议的二阶段提交。

    场景: 模拟现金 + 红包组合支付,假设我们购买了 100 块钱的东西,90块使用现金支付,10 块红包支付,现金和红包处在不同的库。

    假设: 现在有两个库:xa_account(账户库,现金库)、xa_red_account(红包库)。两个库下面都有一张 account 表,account 表中的字段也比较简单,就 id、user_id、balance_amount 三个字段,SQL 我就不贴了。

    public class XaDemo <
    public static void main(String[] args) throws Exception

    // 是否开启日志
    boolean logXaCommands 使用 XA 交易時的指導方針和限制 = true;

    // 获取账户库的 rm(ap做的事情)
    Connection accountConn = DriverManager.getConnection("jdbc:mysql:使用 XA 交易時的指導方針和限制 //106.12.12.xxxx:3306/xa_account?useUnicode=true&characterEncoding=utf8","root","xxxxx");
    XAConnection accConn = new MysqlXAConnection((JdbcConnection) accountConn, logXaCommands);
    XAResource accountRm = accConn.getXAResource();
    // 获取红包库的RM
    Connection redConn = DriverManager.getConnection("jdbc:mysql://106.12.12.xxxx:3306/xa_red_account?useUnicode=true&characterEncoding=utf8","root","xxxxxx");
    XAConnection Conn2 = new MysqlXAConnection((JdbcConnection) redConn, logXaCommands);
    XAResource redRm = Conn2.getXAResource();
    // XA 事务开始了
    // 全局事务 使用 XA 交易時的指導方針和限制
    byte[] globalId = UUID.randomUUID().toString().getBytes();
    // 就一个标识
    int formatId = 1;

    // 账户的分支事务
    byte[] accBqual = UUID.randomUUID().toString().getBytes();;
    Xid xid = new MysqlXid(globalId, accBqual, formatId);

    // 红包分支事务
    byte[] redBqual = UUID.randomUUID().toString().getBytes();;
    Xid xid1 = new MysqlXid(globalId, redBqual, formatId);
    try // 账号事务开始 此时状态:ACTIVE
    accountRm.start(xid, XAResource.TMNOFLAGS);
    // 使用 XA 交易時的指導方針和限制 模拟业务
    String sql = "update account set balance_amount=balance_amount-90 where user_id=1";
    PreparedStatement 使用 XA 交易時的指導方針和限制 ps1 = accountConn.prepareStatement(sql);
    ps1.execute();
    accountRm.end(xid, XAResource.TMSUCCESS);
    // 账号 XA 事务 此时状态:IDLE
    // 红包分支事务开始
    redRm.start(xid1, XAResource.使用 XA 交易時的指導方針和限制 使用 XA 交易時的指導方針和限制 TMNOFLAGS);
    // 模拟业务
    String sql1 = "update account set balance_amount=balance_amount-10 where user_id=1";
    PreparedStatement ps2 = redConn.prepareStatement(sql1);
    ps2.execute();
    redRm.end(xid1, XAResource.TMSUCCESS);


    // 第一阶段:准备提交
    int rm1_prepare = accountRm.prepare(xid);
    int rm2_prepare = redRm.prepare(xid1);

    // XA 事务 此时状态:PREPARED
    // 使用 XA 交易時的指導方針和限制 第二阶段:TM 根据第一阶段的情况决定是提交还是回滚
    boolean onePhase = false; //TM判断有2个事务分支,所以不能优化为一阶段提交
    if (rm1_prepare == XAResource.XA_OK && rm2_prepare == XAResource.XA_OK) accountRm.commit(xid, onePhase);
    redRm.commit(xid1, onePhase);
    > else accountRm.rollback(xid);
    redRm.rollback(xid1);
    >

    > catch (Exception e) // 出现异常,回滚
    accountRm.rollback(xid);
    redRm.rollback(xid1);
    e.printStackTrace();
    >
    >
    >

    运行程序,可以看到如下结果:

    从图中可以清楚看出 XA 事务两阶段提交过程,更多细节请查阅 MySQL 数据库 XA Transactions 模块。

    欢迎关注公众号【互联网平头哥】。关注这个互联网苟且偷生的程序员,愿你我共同进步,今天最好的是明天最低的要求。

    2. 为什么支持 XA?

    img

    思考:

    协议的阻塞机制本身并不是问题,关键问题在于 协议阻塞 遇上 数据锁定。

    如果一个参与全局事务的资源 “失联” 了(收不到分支事务结束的命令),那么它锁定的数据,将一直被锁定。进而,甚至可能因此产生死锁。

    这是 XA 协议的核心痛点,也是 Seata 引入 XA 模式要重点解决的问题。

    基本思路是两个方面:避免 “失联” 和 增加 “自解锁” 机制。(这里涉及非常多技术细节,暂时不展开,在后续 XA 模式演进过程中,会专门拿出来讨论)

    1. 性能差:性能的损耗主要来自两个方面:一方面,事务协调过程,增加单个事务的 RT;另一方面,并发事务数据的锁冲突,降低吞吐。

    思考:

    和不使用分布式事务支持的运行场景比较,性能肯定是下降的,这点毫无疑问。

    本质上,事务(无论是本地事务还是分布式事务)机制就是拿部分 性能的牺牲 ,换来 编程模型的简单 。

    与同为 业务无侵入 的 AT 模式比较:

    首先,因为同样运行在 Seata 定义的分布式事务框架下,XA 模式并没有产生更多事务协调的通信开销。

    其次,并发事务间,如果数据存在热点,产生锁冲突,这种情况,在 AT 模式(默认使用全局锁)下同样存在的。

    所以,在影响性能的两个主要方面,XA 模式并不比 AT 模式有非常明显的劣势。

    AT 模式性能优势主要在于:集中管理全局数据锁,锁的释放不需要 RM 参与,释放锁非常快;另外,全局提交的事务,完成阶段 异步化。

    3. XA 模式如何实现以及怎样用?

    3.1 XA 模式的设计

    3.1.1 设计目标

    1. 从 场景 使用 XA 交易時的指導方針和限制 上,满足 全局一致性 的需求。
    2. 从 应用上,保持与 AT 模式一致的无侵入。
    3. 从 机制 上,适应分布式微服务架构的特点。
    1. 与 AT 模式相同的:以应用程序中 本地事务 的粒度,构建到 XA 模式的 分支事务。
    2. 通过数据源代理,在应用程序本地事务范围外,在框架层面包装 XA 协议的交互机制,把 XA 编程模型 透明化。
    3. 把 XA 的 2PC 拆开,在分支事务 执行阶段 的末尾就进行 XA prepare,把 XA 协议完美融合到 Seata 的事务框架,减少一轮 RPC 交互。

    3.1.2 核心设计

    1. 整体运行机制

    XA 模式 运行在 Seata 定义的事务框架内:

    • XA start/XA end/XA prepare + SQL + 注册分支

    2. 数据源代理

    XA 模式需要 XAConnection。

    获取 XAConnection 两种方式:

    • 方式一:要求开发者配置 XADataSource
    • 方式二:根据开发者的普通 DataSource 来创建

    第一种方式,给开发者增加了认知负担,需要为 XA 模式专门去学习和使用 XA 数据源,与 透明化 XA 编程模型的设计目标相违背。

    第二种方式,对开发者比较友好,和 AT 模式使用一样,开发者完全不必关心 XA 层面的任何问题,保持本地编程模型即可。

    我们优先设计实现第二种方式:数据源代理根据普通数据源中获取的普通 JDBC 连接创建出相应的 XAConnection。

    类比 AT 模式的数据源代理机制,如下:

    img

    综合考虑,XA 模式的数据源代理设计需要同时支持第一种方式:基于 XA 数据源进行代理。

    类比 AT 模式的数据源代理机制,如下:

    img

    3. 分支注册

    XA start 需要 Xid 参数。

    这个 Xid 需要和 Seata 全局事务的 XID 和 BranchId 关联起来,以便由 TC 驱动 XA 分支的提交或回滚。

    目前 Seata 的 BranchId 是在分支注册过程,由 TC 统一生成的,所以 XA 模式分支注册的时机需要在 XA 使用 XA 交易時的指導方針和限制 start 之前。

    把分支注册尽量延后。类似 AT 模式在本地事务提交之前才注册分支,避免分支执行失败情况下,没有意义的分支注册。

    这个优化方向需要 BranchId 生成机制的变化来配合。BranchId 不通过分支注册过程生成,而是生成后再带着 BranchId 去注册分支。

    这里只通过几个重要的核心设计,说明 XA 模式的基本工作机制。

    此外,还有包括 连接保持异常处理 等重要方面,有兴趣可以从项目代码中进一步了解。

    3.1.3 演进规划

    1. 第 1 步(已经完成):首个版本(1.2.0),把 XA 模式原型机制跑通。确保只增加,不修改,不给其他模式引入的新问题。
    2. 第 2 步(计划 5 月完成):与 AT 模式必要的融合、重构。
    3. 第 3 步(计划 7 月完成):完善异常处理机制,进行上生产所必需的打磨。
    4. 第 4 步(计划 8 月完成):性能优化。
    5. 第 5 步(计划 2020 年内完成):结合 Seata 项目正在进行的面向云原生的 Transaction Mesh 设计,打造云原生能力。

    3.2 XA 模式的使用

    从编程模型上,XA 模式与 AT 模式保持完全一致。

    可以参考 使用 XA 交易時的指導方針和限制 Seata 官网的样例:seata-xa

    样例场景是 Seata 经典的,涉及库存、订单、账户 3 个微服务的商品订购业务。

    在样例中,上层编程模型与 AT 模式完全相同。只需要修改数据源代理,即可实现 XA 模式与 AT 模式之间的切换。

    Seata 项目最核心的价值在于:构建一个全面解决分布式事务问题的 标准化 平台。

    img

    XA 模式的加入,补齐了 Seata 在 全局一致性 场景下的缺口,形成 AT、TCC、Saga、XA 四大 事务模式 的版图,基本可以满足所有场景的分布式事务处理诉求。

    当然 XA 模式和 Seata 项目本身都还不尽完美,有很多需要改进和完善的地方。非常欢迎大家参与到项目的建设中,共同打造一个标准化的分布式事务平台。

    基于 XA 事务协议,用代码实现一个二阶段分布式事务

    MySQL 从5.0.3开始支持XA分布式事务,且只有InnoDB存储引擎支持。入下图: 使用 XA 交易時的指導方針和限制

    在 MySQL数据库官网有一个模块专门讲 XA 事务,具体可以查看:

    其他的我就不说了,这里我提一下 XA 事务状态,一个完整的事务流程如下:

    • 1.使用 XA START 来启动一个 使用 XA 交易時的指導方針和限制 使用 XA 交易時的指導方針和限制 XA 事务,并把它置于 ACTIVE 状态。
    • 2.对于一个 ACTIVE 状态的 XA 事务,我们可以执行构成事务的 SQL 语句,然后发布一个 XA END 语句。XA END 把事务放入 IDLE 状态。
    • 3.对于一个 IDLE 状态XA事务,可以执行一个 XA PREPARE 语句或一个XA COMMIT…ONE PHASE 语句:
      • XA PREPARE 把事务放入 PREPARED 状态。在此点上的 XA RECOVER 语句将在其输出中包括事务的 xid 值,因为 XA RECOVER 会列出处于 PREPARED 状态的所有 XA 事务。
      • XA COMMIT…ONE PHASE 用于预备和提交事务。xid 值将不会被 XA RECOVER 列出,因为事务终止。

      总结一下,XA 事务,通过 Start 启动一个 XA 事务,并且被置为 Active 状态,处在 active 状态的事务可以执行 SQL 语句,通过 END 方法将 XA 事务置为 IDLE 状态。处于 IDLE 状态可以执行 PREPARE 操作或者 COMMIT…ONE PHASE 操作,也就是二阶段提交中的第一阶段,PREPARED 使用 XA 交易時的指導方針和限制 状态的 XA事务的时候就可以 Commit 或者 RollBack,也就是二阶段提交的第二阶段

      可能你注意到了上面有一个 XID 值,简单的讲一下,MySQL 中使用xid来作为一个事务分支的标识符。关于 xid 在 XA 规范中有定义,XA规范定义了一个xid有4个部分组成:

      • gtrid:全局事务标识符(global transaction identifier),最大不能超过64字节。
      • bqual:分支限定符(branch qualifier),最大不能超过64字节。
      • data:xid的值,其是 gtrid和bqual拼接后的内容。
      • formatId:formatId的作用就是记录gtrid、bqual的格式,类似于memcached中flags字段的作用。

      好了,关于 XA 事务就 BB 这么多了,接下来,我们通过一个实例,来实现一把基于 XA 事务协议的二阶段提交。

      场景: 模拟现金 + 红包组合支付,假设我们购买了 100 使用 XA 交易時的指導方針和限制 块钱的东西,90块使用现金支付,10 块红包支付,现金和红包处在不同的库。

      假设: 现在有两个库:xa_account(账户库,现金库)、xa_red_account(红包库)。两个库下面都有一张 account 表,account 表中的字段也比较简单,就 id、user_id、balance_amount 三个字段,SQL 我就不贴了。

      public class XaDemo <
      public static void main(String[] args) throws Exception

      // 是否开启日志
      boolean logXaCommands = true;

      // 获取账户库的 rm(ap做的事情)
      Connection accountConn = DriverManager.getConnection("jdbc:mysql://106.12.12.xxxx:3306/xa_account?useUnicode=true&characterEncoding=utf8","root","xxxxx");
      XAConnection accConn = 使用 XA 交易時的指導方針和限制 new MysqlXAConnection((JdbcConnection) accountConn, logXaCommands);
      XAResource accountRm = accConn.getXAResource();
      // 获取红包库的RM
      Connection redConn = DriverManager.getConnection("jdbc:mysql://106.12.12.xxxx:3306/xa_red_account?useUnicode=true&characterEncoding=utf8","root","xxxxxx");
      XAConnection Conn2 = new MysqlXAConnection((JdbcConnection) redConn, logXaCommands);
      XAResource redRm = Conn2.getXAResource();
      // XA 事务开始了
      // 全局事务
      byte[] globalId = UUID.randomUUID().toString().getBytes();
      // 就一个标识
      int formatId = 1;

      // 账户的分支事务
      byte[] accBqual = UUID.randomUUID().toString().getBytes();;
      Xid xid = new MysqlXid(globalId, accBqual, formatId);

      // 红包分支事务
      byte[] redBqual = UUID.randomUUID().toString().getBytes();;
      Xid xid1 = new MysqlXid(globalId, redBqual, formatId);
      try // 账号事务开始 此时状态:ACTIVE
      accountRm.start(xid, XAResource.TMNOFLAGS);
      // 模拟业务
      String sql = "update account set balance_amount=balance_amount-90 where user_id=1";
      PreparedStatement ps1 = accountConn.prepareStatement(sql);
      ps1.execute();
      accountRm.end(xid, XAResource.TMSUCCESS);
      // 账号 XA 事务 此时状态:IDLE
      // 红包分支事务开始
      redRm.start(xid1, XAResource.TMNOFLAGS);
      // 模拟业务
      String sql1 = "update account set balance_amount=balance_amount-10 where user_id=1";
      PreparedStatement ps2 = redConn.prepareStatement(sql1);
      ps2.execute();
      redRm.end(xid1, XAResource.TMSUCCESS);


      // 第一阶段:准备提交
      int rm1_prepare = accountRm.prepare(xid);
      int rm2_prepare = redRm.prepare(xid1);

      // XA 事务 此时状态:PREPARED
      // 第二阶段:TM 根据第一阶段的情况决定是提交还是回滚
      boolean onePhase = false; //TM判断有2个事务分支,所以不能优化为一阶段提交
      if (rm1_prepare == XAResource.XA_OK && rm2_prepare == XAResource.XA_OK) accountRm.commit(xid, onePhase);
      redRm.commit(使用 XA 交易時的指導方針和限制 xid1, onePhase);
      > else accountRm.rollback(xid);
      redRm.rollback(xid1);
      >

      > catch (使用 XA 交易時的指導方針和限制 Exception e) // 出现异常,回滚
      accountRm.rollback(xid);
      redRm.rollback(xid1);
      e.printStackTrace();
      >
      >
      >

      运行程序,可以看到如下结果:

      从图中可以清楚看出 XA 事务两阶段提交过程,更多细节请查阅 MySQL 数据库 XA Transactions 模块。

      欢迎关注公众号【互联网平头哥】。关注这个互联网苟且偷生的程序员,愿你我共同进步,今天最好的是明天最低的要求。

      ShardingSphere的分布式事务

      image-20201118192610726

      然后,我们下载并启动 Seata 服务器,这个过程需要设置 Seata 服务器 config 目录下的 registry.conf,以便指定注册中心,这里使用 ZooKeeper 来充当注册中心。关于如何启动 Seata 服务器的过程可以参考 Seata 的官方文档。请注意,按照 Seata 的运行要求,我们需要在每一个分片数据库实例中创建一张 undo_log 表。然后,我们还需要在代码工程中 classpath 中增加一个 seata.conf 配置文件:

      实现 BASE 事务

      基于 ShardingSphere 提供的分布式事务的抽象,我们从 XA 事务转到 BASE 事务唯一要做的事情就是重新设置 TransactionType,也就是修改一行代码: