使用分布式事务 Seata 的 XA 模式

发布时间 2023-12-03 14:32:24作者: 乔京飞

上篇博客已经搭建了分布式事务 Seata 的集群,本篇博客主要介绍如何使用 Seata 的 XA 模式。

XA 模式的规范是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,XA 模式规范描述了全局的 TM 与局部的 RM 之间的接口,几乎所有主流关系型数据库都对 XA 模式的规范提供了支持。

其实现原理就是两阶段的提交:

  • 第一阶段事务协调者通知每个事物参与者执行本地事务,本地事务执行完成后报告事务执行状态给事务协调者,此时事务不提交,继续持有数据库锁
  • 第二阶段根据第一阶段的执行结果而决定。如果一阶段都成功,则通知所有事务参与者,提交事务;如果一阶段任意一个参与者失败,则通知所有事务参与者回滚事务

总体来说,XA 模式是 Seata 中最简单的一种模式。本篇博客通过代码的方式介绍如何使用 XA 模式。


一、搭建工程

参考官方的 Demo ,新建一个 Spring Cloud 工程,结构如下:

image

包含 3 个子工程:账户服务 AccountService、订单服务 OrderService、库存服务 StockService。

由于 3 个子工程基本上都引用了相同的依赖,因此这些依赖都可以放到父工程中,因此父工程的 pom 文件内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.jobs</groupId>
    <artifactId>springcloud_seata_xa</artifactId>
    <packaging>pom</packaging>
    <version>1.0</version>
    <modules>
        <module>AccountService</module>
        <module>OrderService</module>
        <module>StockService</module>
    </modules>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.12.RELEASE</version>
    </parent>

    <dependencyManagement>
        <dependencies>
            <!-- 引入 springCloud 依赖-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR10</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--引入 springCloud alibaba 依赖-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2.2.9.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!-- mysql驱动 -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>8.0.33</version>
            </dependency>
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
                <version>3.5.0</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>
        <!--引入 seata 依赖包-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <exclusions>
                <exclusion>
                    <artifactId>seata-spring-boot-starter</artifactId>
                    <groupId>io.seata</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
            <version>1.8.0</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.26</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.3.12.RELEASE</version>
            </plugin>
        </plugins>
    </build>
</project>

这里最主要的是引入了 spring-cloud-starter-alibaba-seata 依赖包,由于其内部的 seata-spring-boot-starter 版本较低,因此排除其内部的版本,额外引入了与我们上篇博客所搭建的 Seata 集群版本相同的依赖包版本,版本是 1.8.0


二、数据库表介绍

本篇博客模拟了 3 个微服务,操作同一个数据库中的 3 个表,每个微服务操作一张表。

  • AccountService 操作 tb_account 表,主要是扣减金额
  • OrderService 操作 tb_order 表,主要是创建订单
  • StockService 操作 tb_stock 表,主要是扣减库存量

在实际工作中,很可能是不同的微服务对应不同的数据库。本篇 Demo 中的 SQL 脚本如下:

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for tb_account
-- ----------------------------
DROP TABLE IF EXISTS `tb_account`;
CREATE TABLE `tb_account`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户id',
  `money` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '账户金额',
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `user_id`(`user_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of tb_account
-- ----------------------------
INSERT INTO `tb_account` VALUES (1, 'user20231212', 1000);

-- ----------------------------
-- Table structure for tb_order
-- ----------------------------
DROP TABLE IF EXISTS `tb_order`;
CREATE TABLE `tb_order`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户id',
  `goods_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '商品id',
  `count` int(11) NULL DEFAULT NULL COMMENT '商品下单数量',
  `money` int(11) NULL DEFAULT NULL COMMENT '商品下单总金额',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of tb_order
-- ----------------------------

-- ----------------------------
-- Table structure for tb_stock
-- ----------------------------
DROP TABLE IF EXISTS `tb_stock`;
CREATE TABLE `tb_stock`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `goods_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '商品id',
  `count` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '商品库存数量',
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `goods_id`(`goods_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of tb_stock
-- ----------------------------
INSERT INTO `tb_stock` VALUES (1, 'goods20231212', 10);

SET FOREIGN_KEY_CHECKS = 1;

需要注意的是:为了防止【账户金额】和【商品库存量】扣减成负数,所以针对这 2 个字段,设置为无符号整数。这样一旦这 2 个字段扣减的结果为负数时,程序就会抛出异常,结合 Seata 分布式事务的回滚操作,确保数据的一致性。


三、微服务的配置

以订单服务为例,其它服务的配置基本上一模一样,其 application.yml 配置内容如下:

server:
  port: 9090
spring:
  application:
    name: order-service
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://192.168.136.128:3306/seatatest?characterEncoding=utf8&allowMultiQueries=true&useSSL=false
    username: root
    password: root
  cloud:
    nacos:
      server-addr: 192.168.136.128:8848

seata:
  registry:
    type: nacos
    nacos:
      server-addr: 192.168.136.128:8848
      # 空字符串表示使用 nacos 的默认 namespace(public)
      namespace: ""
      group: SEATA_GROUP
      application: seata-server
      #username: nacos
      #password: nacos
  tx-service-group: myseata_test
  service:
    vgroup-mapping:
      myseata_test: jobs
  # 这里配置 Seata 使用 XA 模式
  data-source-proxy-mode: XA

通过 seata 下的 registry 相关配置,从 nacos 中获取 seata 服务的地址,由于我们部署的是 seata 集群,,因此必须配置 seata 集群的服务名称:seata-server。事务组的名称可以自己定义,这里配置为 myseata_test ,需要对应到 nacos 中配置的 seata 集群名称 jobs,最后就是通过 data-source-proxy-mode 来配置默认使用的分布式事务模式,这里配置为 XA

最后就是在程序代码中,需要使用分布式事务的方法上,增加上 @GlobalTransactional 注解即可。本篇博客的 Demo 主要在创建订单的方法上使用分布式事务,该方法执行的逻辑有:创建订单记录、调用 Account 服务扣减金额、调用 Stock 服务扣减库存量,其代码如下:

package com.jobs.service;

import com.jobs.feign.AccountClient;
import com.jobs.feign.StockClient;
import com.jobs.mapper.OrderMapper;
import com.jobs.pojo.Order;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private AccountClient accountClient;
    @Autowired
    private StockClient stockClient;

    @GlobalTransactional
    public Long createOrder(Order order) {
        //创建订单
        orderMapper.insert(order);
        //减钱
        accountClient.minus(order.getUserId(), order.getMoney());
        //减库存
        stockClient.minus(order.getGoodsId(), order.getCount());
        //返回订单号
        return order.getId();
    }
}

最后对外提供一个接口,可以使用 postman 进行调用测试:

package com.jobs.controller;

import com.jobs.pojo.Order;
import com.jobs.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RequestMapping("/order")
@RestController
public class OrderController {

    @Autowired
    private OrderService orderService;

    @PostMapping("/create")
    public ResponseEntity<Long> createOrder(@RequestBody Order order) {
        try {
            Long orderId = orderService.createOrder(order);
            log.info("controller下单成功");
            return ResponseEntity.ok(orderId);
        } catch (Exception ex) {
            log.error("controller下单失败:{}", ex.getMessage());
            return ResponseEntity.status(500).body(0L);
        }
    }
}

四、验证结果

使用 Postman 调用 Order 服务的创建订单接口,测试分布式事务:

(1)首先正常调用接口,要扣减的金额 和 库存,都能够满足,则能够下单成功,返回订单的 id

image

在 AccountService、OrderService、StockService 的控制台日志中,都可以看到成功的日志。

(2)将库存值调大,超过总库存量,然后调用下单接口,则无法下单成功。由于有 Seata 控制分布式事务,添加的订单记录以及扣减的金额,都自动回滚了,确保了数据的一致性。

image

在 AccountService 和 OrderService 的日志中,都能够看到 Branch Rollbacked result: PhaseTwo_Rollbacked 这句话,说明已经回滚数据。


OK,以上就是分布式事务 Seata 的 XA 模式介绍,总体来说 XA 是 Seata 中使用起来最简单的一种模式。

XA模式的优点是:

  • 事务的强一致性,满足 ACID 原则。
  • 常用的关系型数据库都支持,实现简单,并且没有代码侵入

XA模式的缺点是:

  • 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,所以性能相对不高
  • 而且依赖关系型数据库实现事务,不能用于 NoSql 数据库

本篇博客的源代码下载地址为:https://files.cnblogs.com/files/blogs/699532/springcloud_seata_xa.zip