使用分布式事务 Seata 的 XA 模式
上篇博客已经搭建了分布式事务 Seata 的集群,本篇博客主要介绍如何使用 Seata 的 XA 模式。
XA 模式的规范是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,XA 模式规范描述了全局的 TM 与局部的 RM 之间的接口,几乎所有主流关系型数据库都对 XA 模式的规范提供了支持。
其实现原理就是两阶段的提交:
- 第一阶段事务协调者通知每个事物参与者执行本地事务,本地事务执行完成后报告事务执行状态给事务协调者,此时事务不提交,继续持有数据库锁
- 第二阶段根据第一阶段的执行结果而决定。如果一阶段都成功,则通知所有事务参与者,提交事务;如果一阶段任意一个参与者失败,则通知所有事务参与者回滚事务
总体来说,XA 模式是 Seata 中最简单的一种模式。本篇博客通过代码的方式介绍如何使用 XA 模式。
一、搭建工程
参考官方的 Demo ,新建一个 Spring Cloud 工程,结构如下:
包含 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
在 AccountService、OrderService、StockService 的控制台日志中,都可以看到成功的日志。
(2)将库存值调大,超过总库存量,然后调用下单接口,则无法下单成功。由于有 Seata 控制分布式事务,添加的订单记录以及扣减的金额,都自动回滚了,确保了数据的一致性。
在 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