seata AT、XA、TCC

┌─────────────┐         ┌─────────────┐         ┌─────────────┐
│    TM       │         │    TC       │         │    RM       │
│ (订单服务)  │         │ (Seata Server)     │ (库存服务)  │
└──────┬──────┘         └──────┬──────┘         └──────┬──────┘
       │                       │                       │
       │ 1. 开启全局事务        │                       │
       ├──────────────────────>│                       │
       │                       │                       │
       │ 2. 返回 xid           │                       │
       │<──────────────────────┤                       │
       │                       │                       │
       │ 3. 调用 RM(携带 xid) │                       │
       ├──────────────────────────────────────────────>│
       │                       │                       │
       │                       │ 4. 注册分支事务        │
       │                       │<──────────────────────┤
       │                       │                       │
       │                       │ 5. 执行一阶段 + 反馈结果│
       │                       │<──────────────────────┤
       │                       │                       │
       │                       │ 6. 判断全局状态(提交/回滚)│
       │                       │(内部逻辑)            │
       │                       │                       │
       │                       │ 7. 发送二阶段指令      │
       │                       ├──────────────────────>│
       │                       │                       │
       │                       │ 8. 反馈二阶段结果      │
       │                       │<──────────────────────┤
       │                       │                       │
       │ 9. 通知 TM 最终结果    │                       │
       │<──────────────────────┤                       │
       │                       │                       │
模式核心配置 / 注解数据源代理依赖表配置文件指定模式
AT@GlobalTransactionalDataSourceProxyundo_log无需(默认适配)或者seata.data-source-proxy-mode: AT
TCC@TwoPhaseBusinessAction无需代理(原始数据源)无(手动补偿)无需(通过注解识别)
2PC(XA)@GlobalTransactionalDataSourceProxyXA无(依赖数据库 XA)seata.data-source-proxy-mode: XA

---AT:

Seata Server 配置(事务协调器)

1. 下载并配置 Seata Server
  • 下载 Seata 1.6.1:官网地址
  • 解压后修改 conf/registry.conf(注册到 Nacos):

    yaml

    registry:
      type: nacos
      nacos:
        server-addr: 127.0.0.1:8848
        group: SEATA_GROUP
        namespace: ""
        username: nacos
        password: nacos
    
    config:
      type: nacos
      nacos:
        server-addr: 127.0.0.1:8848
        group: SEATA_GROUP
        namespace: ""
        username: nacos
        password: nacos
    
2. 在 Nacos 中添加 Seata 核心配置
  • 新增配置:Data ID=seataServer.propertiesGroup=SEATA_GROUP
  • 配置内容(存储模式设为 DB):

    properties

    store.mode=db
    store.db.datasource=druid
    store.db.dbType=mysql
    store.db.driverClassName=com.mysql.cj.jdbc.Driver
    store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true
    store.db.user=root
    store.db.password=123456  # 替换为你的密码
    store.db.globalTable=global_table
    store.db.branchTable=branch_table
    store.db.lockTable=lock_table
    
3. 初始化 Seata 数据库表

在 seata 库中执行以下 SQL(全局事务表):

sql

CREATE TABLE IF NOT EXISTS `global_table` (
  `xid` VARCHAR(128) NOT NULL,
  `transaction_id` BIGINT,
  `status` TINYINT NOT NULL,
  `application_id` VARCHAR(32),
  `transaction_service_group` VARCHAR(32),
  `transaction_name` VARCHAR(128),
  `timeout` INT,
  `begin_time` BIGINT,
  `application_data` VARCHAR(2000),
  `gmt_create` DATETIME,
  `gmt_modified` DATETIME,
  PRIMARY KEY (`xid`),
  KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
  KEY `idx_transaction_id` (`transaction_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE IF NOT EXISTS `branch_table` (
  `branch_id` BIGINT NOT NULL,
  `xid` VARCHAR(128) NOT NULL,
  `transaction_id` BIGINT,
  `resource_group_id` VARCHAR(32),
  `resource_id` VARCHAR(256),
  `branch_type` VARCHAR(8),
  `status` TINYINT,
  `client_id` VARCHAR(64),
  `application_data` VARCHAR(2000),
  `gmt_create` DATETIME(6),
  `gmt_modified` DATETIME(6),
  PRIMARY KEY (`branch_id`),
  KEY `idx_xid` (`xid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE IF NOT EXISTS `lock_table` (
  `row_key` VARCHAR(128) NOT NULL,
  `xid` VARCHAR(128),
  `transaction_id` BIGINT,
  `branch_id` BIGINT NOT NULL,
  `resource_id` VARCHAR(256),
  `table_name` VARCHAR(32),
  `pk` VARCHAR(36),
  `status` TINYINT NOT NULL DEFAULT '0',
  `gmt_create` DATETIME,
  `gmt_modified` DATETIME,
  PRIMARY KEY (`row_key`),
  KEY `idx_xid` (`xid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
4. 启动 Seata Server

bash

# Linux/Mac
sh bin/seata-server.sh -p 8091 -h 127.0.0.1 -m db

# Windows
bin/seata-server.bat -p 8091 -h 127.0.0.1 -m db

三、订单服务(order-service)

1. pom.xml 依赖

xml

<?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>

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

    <groupId>com.example</groupId>
    <artifactId>order-service</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <!-- Spring Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- MyBatis -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>
        <!-- MySQL 驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- Seata -->
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
            <version>1.6.1</version>
        </dependency>
        <!-- Spring Cloud Alibaba Nacos 注册 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <version>2.2.7.RELEASE</version>
        </dependency>
        <!-- OpenFeign(服务调用) -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <version>2.2.9.RELEASE</version>
        </dependency>
    </dependencies>
</project>
2. 配置文件(application.yml)

yaml

server:
  port: 8081  # 订单服务端口

spring:
  application:
    name: order-service  # 服务名
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/order_db?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
    username: root
    password: 123456  # 替换为你的密码
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848  # Nacos 地址

mybatis:
  mapper-locations: classpath:mapper/*.xml  # MyBatis 映射文件路径
  type-aliases-package: com.example.order.entity  # 实体类包路径

seata:
  enabled: true
  application-id: ${spring.application.name}
  tx-service-group: my_test_tx_group  # 事务组,需与 Seata Server 一致
  data-source-proxy-mode: AT  # 显式指定 AT 模式
  service:
    vgroup-mapping:
      my_test_tx_group: default  # 事务组映射到 Seata 集群
    grouplist:
      default: 127.0.0.1:8091  # Seata Server 地址
  registry:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
3. 数据源代理配置(AT 模式核心)

java

运行

package com.example.order.config;

import com.zaxxer.hikari.HikariDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;

@Configuration
public class DataSourceConfig {

    // 配置原始数据源
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource originalDataSource() {
        return new HikariDataSource();
    }

    // 用 Seata DataSourceProxy 代理数据源(AT 模式必须)
    @Bean
    public DataSourceProxy dataSourceProxy(DataSource originalDataSource) {
        return new DataSourceProxy(originalDataSource);
    }

    // 配置 MyBatis 使用代理数据源
    @Bean
    public SqlSessionFactoryBean sqlSessionFactory(DataSourceProxy dataSourceProxy) throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSourceProxy);  // 使用代理数据源
        factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources("classpath:mapper/*.xml"));  // 映射文件路径
        factoryBean.setTransactionFactory(new SpringManagedTransactionFactory());  // 事务管理
        return factoryBean;
    }
}
4. 实体类与 Mapper
(1)订单实体(Order.java)

java

运行

package com.example.order.entity;

public class Order {
    private Long id;
    private String userId;
    private String productId;
    private Integer count;
    private String status;  // 订单状态:PENDING/COMPLETED

    // 构造器、getter、setter
    public Order() {}
    public Order(String userId, String productId, Integer count, String status) {
        this.userId = userId;
        this.productId = productId;
        this.count = count;
        this.status = status;
    }

    // getter 和 setter 省略
}
(2)OrderMapper.java

java

运行

package com.example.order.mapper;

import com.example.order.entity.Order;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface OrderMapper {
    void insert(Order order);
}
(3)OrderMapper.xml(classpath:mapper/OrderMapper.xml)

xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.order.mapper.OrderMapper">
    <insert id="insert" parameterType="com.example.order.entity.Order">
        INSERT INTO `order` (user_id, product_id, count, status)
        VALUES (#{userId}, #{productId}, #{count}, #{status})
    </insert>
</mapper>
5. Feign 客户端(调用库存服务)

java

运行

package com.example.order.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(name = "inventory-service")  // 库存服务名
public interface InventoryFeignClient {

    // 调用库存服务扣减接口
    @PostMapping("/inventory/deduct")
    String deduct(@RequestParam("productId") String productId, @RequestParam("count") Integer count);
}
6. 服务层与控制层
(1)OrderService.java

java

运行

package com.example.order.service;

import com.example.order.entity.Order;
import com.example.order.feign.InventoryFeignClient;
import com.example.order.mapper.OrderMapper;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private InventoryFeignClient inventoryFeignClient;

    // AT 模式全局事务(标记 @GlobalTransactional)
    @GlobalTransactional(rollbackFor = Exception.class)
    public void createOrder(String userId, String productId, Integer count) {
        // 1. 创建订单(本地事务)
        Order order = new Order(userId, productId, count, "PENDING");
        orderMapper.insert(order);
        System.out.println("订单创建成功:" + order);

        // 2. 调用库存服务扣减库存(远程事务)
        String result = inventoryFeignClient.deduct(productId, count);
        if (!"success".equals(result)) {
            throw new RuntimeException("库存扣减失败,触发全局回滚");
        }

        // 3. 模拟异常(测试回滚)
        // if (true) throw new RuntimeException("测试回滚");
    }
}
(2)OrderController.java

java

运行

package com.example.order.controller;

import com.example.order.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderController {

    @Autowired
    private OrderService orderService;

    @PostMapping("/order/create")
    public String createOrder(
            @RequestParam String userId,
            @RequestParam String productId,
            @RequestParam Integer count) {
        try {
            orderService.createOrder(userId, productId, count);
            return "订单创建成功";
        } catch (Exception e) {
            return "订单创建失败:" + e.getMessage();
        }
    }
}
7. 启动类

java

运行

package com.example.order;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients  // 启用 Feign 客户端
public class OrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
}
8. 初始化订单库表

在 order_db 库中执行:

sql

-- 订单表
CREATE TABLE `order` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` varchar(32) NOT NULL,
  `product_id` varchar(32) NOT NULL,
  `count` int(11) NOT NULL,
  `status` varchar(32) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- AT 模式回滚日志表(必须)
CREATE TABLE IF NOT EXISTS `undo_log` (
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  PRIMARY KEY (`branch_id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

四、库存服务(inventory-service)

1. pom.xml 依赖

与订单服务一致(端口和服务名不同)。

2. 配置文件(application.yml)

yaml

server:
  port: 8082  # 库存服务端口

spring:
  application:
    name: inventory-service  # 服务名
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/inventory_db?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
    username: root
    password: 123456  # 替换为你的密码
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848

mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.example.inventory.entity

seata:
  enabled: true
  application-id: ${spring.application.name}
  tx-service-group: my_test_tx_group  # 与订单服务同一事务组
  data-source-proxy-mode: AT  # 显式指定 AT 模式
  service:
    vgroup-mapping:
      my_test_tx_group: default
    grouplist:
      default: 127.0.0.1:8091
  registry:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
3. 数据源代理配置

与订单服务的 DataSourceConfig 完全一致(确保使用 DataSourceProxy)。

4. 实体类与 Mapper
(1)库存实体(Inventory.java)

java

运行

package com.example.inventory.entity;

public class Inventory {
    private Long id;
    private String productId;
    private Integer stock;  // 可用库存

    // getter 和 setter 省略
}
(2)InventoryMapper.java

java

运行

package com.example.inventory.mapper;

import com.example.inventory.entity.Inventory;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface InventoryMapper {
    Inventory selectByProductId(@Param("productId") String productId);
    void updateStock(@Param("productId") String productId, @Param("count") Integer count);
}
(3)InventoryMapper.xml(classpath:mapper/InventoryMapper.xml)

xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.inventory.mapper.InventoryMapper">
    <select id="selectByProductId" resultType="com.example.inventory.entity.Inventory">
        SELECT * FROM inventory WHERE product_id = #{productId}
    </select>

    <update id="updateStock">
        UPDATE inventory SET stock = stock - #{count} WHERE product_id = #{productId}
    </update>
</mapper>
5. 服务层与控制层
(1)InventoryService.java

java

运行

package com.example.inventory.service;

import com.example.inventory.entity.Inventory;
import com.example.inventory.mapper.InventoryMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class InventoryService {

    @Autowired
    private InventoryMapper inventoryMapper;

    // 扣减库存(本地事务)
    @Transactional
    public void deductStock(String productId, Integer count) {
        // 1. 查询库存
        Inventory inventory = inventoryMapper.selectByProductId(productId);
        if (inventory == null) {
            throw new RuntimeException("商品不存在");
        }
        // 2. 检查库存是否充足
        if (inventory.getStock() < count) {
            throw new RuntimeException("库存不足");
        }
        // 3. 扣减库存
        inventoryMapper.updateStock(productId, count);
        System.out.println("库存扣减成功:" + productId + ",扣减数量:" + count);
    }
}
(2)InventoryController.java

java

运行

package com.example.inventory.controller;

import com.example.inventory.service.InventoryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/inventory")
public class InventoryController {

    @Autowired
    private InventoryService inventoryService;

    @PostMapping("/deduct")
    public String deduct(@RequestParam String productId, @RequestParam Integer count) {
        try {
            inventoryService.deductStock(productId, count);
            return "success";
        } catch (Exception e) {
            return "fail: " + e.getMessage();
        }
    }
}
6. 启动类

java

运行

package com.example.inventory;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class InventoryApplication {
    public static void main(String[] args) {
        SpringApplication.run(InventoryApplication.class, args);
    }
}
7. 初始化库存库表

在 inventory_db 库中执行:

sql

-- 库存表
CREATE TABLE `inventory` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `product_id` varchar(32) NOT NULL,
  `stock` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_product_id` (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 初始化库存数据
INSERT INTO `inventory` (`product_id`, `stock`) VALUES ('P001', 100);

-- AT 模式回滚日志表(必须)
CREATE TABLE IF NOT EXISTS `undo_log` (
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  PRIMARY KEY (`branch_id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

五、测试流程

  1. 启动 Nacos:startup.sh -m standalone
  2. 启动 Seata Server(按步骤二)
  3. 启动库存服务和订单服务
  4. 调用订单创建接口:POST http://localhost:8081/order/create?userId=U001&productId=P001&count=10
  5. 正常情况:订单表新增记录,库存表 P001 库存减少 10
  6. 测试回滚:取消订单服务 createOrder 方法中异常代码的注释,再次调用接口,观察订单和库存是否回滚(均恢复原值)

核心配置说明

  • AT 模式必须DataSourceProxy 数据源代理 + undo_log 表 + @GlobalTransactional 注解
  • 显式指定模式data-source-proxy-mode: AT 确保 Seata 按 AT 模式处理
  • 事务组一致:订单和库存服务的 tx-service-group 必须相同,否则无法纳入同一全局事务

通过以上配置和代码,Seata AT 模式可实现分布式事务的自动提交和回滚,无需手动编写补偿逻辑。

-----XA:

订单服务(order-service-xa)

1. pom.xml 依赖

xml

<?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>

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

    <groupId>com.example</groupId>
    <artifactId>order-service-xa</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <!-- Spring Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- MyBatis -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>
        <!-- MySQL 驱动(支持 XA) -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- Seata -->
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
            <version>1.6.1</version>
        </dependency>
        <!-- Nacos 注册 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <version>2.2.7.RELEASE</version>
        </dependency>
        <!-- OpenFeign -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <version>2.2.9.RELEASE</version>
        </dependency>
    </dependencies>
</project>
2. 配置文件(application.yml)

yaml

server:
  port: 8081  # 订单服务端口

spring:
  application:
    name: order-service-xa  # 服务名
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/order_db_xa?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
    username: root
    password: 123456  # 替换为你的密码
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848

mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.example.order.entity

seata:
  enabled: true
  application-id: ${spring.application.name}
  tx-service-group: my_test_tx_group  # 事务组,与 Seata Server 一致
  data-source-proxy-mode: XA  # 显式指定 XA 模式(核心)
  service:
    vgroup-mapping:
      my_test_tx_group: default
    grouplist:
      default: 127.0.0.1:8091
  registry:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
  # XA 模式特有配置:超时后降级为异步提交(可选)
  xa:
    data-source-proxy-mode: XA
    recovery:
      commit-retry-count: 5
      rollback-retry-count: 5
3. XA 数据源代理配置(核心)

java

运行

package com.example.order.config;

import com.mysql.cj.jdbc.MysqlXADataSource;
import io.seata.rm.datasource.DataSourceProxyXA;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;

@Configuration
public class XADataSourceConfig {

    // 配置 MySQL XA 数据源(支持 XA 协议)
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource originalDataSource() {
        // 使用 MySQL 官方 XA 数据源
        MysqlXADataSource xaDataSource = new MysqlXADataSource();
        // 配置会自动绑定到 spring.datasource 的属性(url、username、password)
        return xaDataSource;
    }

    // 用 Seata XA 代理包装数据源(XA 模式必须)
    @Bean
    public DataSourceProxyXA dataSourceProxyXA(DataSource originalDataSource) {
        return new DataSourceProxyXA(originalDataSource);
    }

    // 配置 MyBatis 使用 XA 代理数据源
    @Bean
    public SqlSessionFactoryBean sqlSessionFactory(DataSourceProxyXA dataSourceProxyXA) throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSourceProxyXA);  // 使用 XA 代理数据源
        factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources("classpath:mapper/*.xml"));
        factoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
        return factoryBean;
    }
}
4. 实体类、Mapper 与业务代码
(1)订单实体(Order.java)

同 AT 模式,包含 iduserIdproductIdcountstatus 字段。

(2)OrderMapper.java 与 Mapper.xml

java

运行

// OrderMapper.java
package com.example.order.mapper;

import com.example.order.entity.Order;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface OrderMapper {
    void insert(Order order);
}

xml

<!-- OrderMapper.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.order.mapper.OrderMapper">
    <insert id="insert" parameterType="com.example.order.entity.Order">
        INSERT INTO `order` (user_id, product_id, count, status)
        VALUES (#{userId}, #{productId}, #{count}, #{status})
    </insert>
</mapper>
(3)Feign 客户端(调用库存服务)

java

运行

package com.example.order.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(name = "inventory-service-xa")  // 库存服务名
public interface InventoryFeignClient {
    @PostMapping("/inventory/deduct")
    String deduct(@RequestParam("productId") String productId, @RequestParam("count") Integer count);
}
(4)服务层与控制层

java

运行

// OrderService.java
package com.example.order.service;

import com.example.order.entity.Order;
import com.example.order.feign.InventoryFeignClient;
import com.example.order.mapper.OrderMapper;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private InventoryFeignClient inventoryFeignClient;

    // XA 模式全局事务(@GlobalTransactional 标记)
    @GlobalTransactional(rollbackFor = Exception.class)
    public void createOrder(String userId, String productId, Integer count) {
        // 1. 创建订单(本地 XA 事务)
        Order order = new Order(userId, productId, count, "PENDING");
        orderMapper.insert(order);
        System.out.println("订单创建成功:" + order);

        // 2. 调用库存服务扣减库存(远程 XA 事务)
        String result = inventoryFeignClient.deduct(productId, count);
        if (!"success".equals(result)) {
            throw new RuntimeException("库存扣减失败,触发全局回滚");
        }

        // 3. 模拟异常(测试回滚)
        // if (true) throw new RuntimeException("测试 XA 回滚");
    }
}

java

运行

// OrderController.java
package com.example.order.controller;

import com.example.order.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

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

    @Autowired
    private OrderService orderService;

    @PostMapping("/create")
    public String createOrder(
            @RequestParam String userId,
            @RequestParam String productId,
            @RequestParam Integer count) {
        try {
            orderService.createOrder(userId, productId, count);
            return "订单创建成功(XA 模式)";
        } catch (Exception e) {
            return "订单创建失败:" + e.getMessage();
        }
    }
}
(5)启动类

java

运行

package com.example.order;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients
public class OrderXaApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderXaApplication.class, args);
    }
}
5. 初始化订单库表(order_db_xa)

sql

-- 订单表
CREATE TABLE `order` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` varchar(32) NOT NULL,
  `product_id` varchar(32) NOT NULL,
  `count` int(11) NOT NULL,
  `status` varchar(32) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- XA 模式不需要 undo_log 表(依赖数据库 XA 协议回滚)

四、库存服务(inventory-service-xa)

1. pom.xml 依赖

与订单服务一致。

2. 配置文件(application.yml)

yaml

server:
  port: 8082  # 库存服务端口

spring:
  application:
    name: inventory-service-xa  # 服务名
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/inventory_db_xa?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
    username: root
    password: 123456  # 替换为你的密码
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848

mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.example.inventory.entity

seata:
  enabled: true
  application-id: ${spring.application.name}
  tx-service-group: my_test_tx_group  # 同一事务组
  data-source-proxy-mode: XA  # 显式指定 XA 模式
  service:
    vgroup-mapping:
      my_test_tx_group: default
    grouplist:
      default: 127.0.0.1:8091
  registry:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
3. XA 数据源代理配置

与订单服务的 XADataSourceConfig 完全一致(使用 MysqlXADataSource 和 DataSourceProxyXA)。

4. 实体类、Mapper 与业务代码
(1)库存实体(Inventory.java)

包含 idproductIdstock 字段。

(2)InventoryMapper.java 与 Mapper.xml

java

运行

// InventoryMapper.java
package com.example.inventory.mapper;

import com.example.inventory.entity.Inventory;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface InventoryMapper {
    Inventory selectByProductId(@Param("productId") String productId);
    void updateStock(@Param("productId") String productId, @Param("count") Integer count);
}

xml

<!-- InventoryMapper.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.inventory.mapper.InventoryMapper">
    <select id="selectByProductId" resultType="com.example.inventory.entity.Inventory">
        SELECT * FROM inventory WHERE product_id = #{productId}
    </select>

    <update id="updateStock">
        UPDATE inventory SET stock = stock - #{count} WHERE product_id = #{productId}
    </update>
</mapper>
(3)服务层与控制层

java

运行

// InventoryService.java
package com.example.inventory.service;

import com.example.inventory.entity.Inventory;
import com.example.inventory.mapper.InventoryMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class InventoryService {

    @Autowired
    private InventoryMapper inventoryMapper;

    // 扣减库存(本地 XA 事务)
    @Transactional
    public void deductStock(String productId, Integer count) {
        Inventory inventory = inventoryMapper.selectByProductId(productId);
        if (inventory == null) {
            throw new RuntimeException("商品不存在");
        }
        if (inventory.getStock() < count) {
            throw new RuntimeException("库存不足");
        }
        inventoryMapper.updateStock(productId, count);
        System.out.println("库存扣减成功:" + productId + ",扣减数量:" + count);
    }
}

java

运行

// InventoryController.java
package com.example.inventory.controller;

import com.example.inventory.service.InventoryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;

@RestController
@RequestMapping("/inventory")
public class InventoryController {

    @Autowired
    private InventoryService inventoryService;

    @PostMapping("/deduct")
    public String deduct(@RequestParam String productId, @RequestParam Integer count) {
        try {
            inventoryService.deductStock(productId, count);
            return "success";
        } catch (Exception e) {
            return "fail: " + e.getMessage();
        }
    }
}
(4)启动类

java

运行

package com.example.inventory;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class InventoryXaApplication {
    public static void main(String[] args) {
        SpringApplication.run(InventoryXaApplication.class, args);
    }
}
5. 初始化库存库表(inventory_db_xa)

sql

-- 库存表
CREATE TABLE `inventory` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `product_id` varchar(32) NOT NULL,
  `stock` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_product_id` (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 初始化库存数据
INSERT INTO `inventory` (`product_id`, `stock`) VALUES ('P001', 100);

五、测试流程

  1. 启动 Nacos 和 Seata Server(同 AT 模式)。
  2. 启动库存服务和订单服务。
  3. 调用订单接口:POST http://localhost:8081/order/create?userId=U001&productId=P001&count=10
  4. 正常情况:订单表新增记录,库存表 P001 库存减少 10。
  5. 测试回滚:取消订单服务中异常代码的注释,再次调用,观察订单和库存是否回滚(依赖 MySQL XA 协议回滚)。

XA 模式核心配置说明

  1. 数据源代理:必须使用 MysqlXADataSource(MySQL XA 驱动)和 DataSourceProxyXA(Seata XA 代理)。
  2. 显式模式指定data-source-proxy-mode: XA 是 XA 模式的关键标识,与 AT 模式区分。
  3. 无需 undo_log 表:XA 模式依赖数据库自身的 XA 协议回滚,不需要 undo_log 表。
  4. 全局事务注解:仍使用 @GlobalTransactional 标记事务入口,Seata 会按 XA 协议协调二阶段提交。

通过以上配置,Seata XA 模式可基于数据库 XA 协议实现强一致性的分布式事务,适合对一致性要求极高的场景(如金融交易)。

----TCC:

订单服务(order-service-tcc)

1. pom.xml 依赖

xml

<?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>

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

    <groupId>com.example</groupId>
    <artifactId>order-service-tcc</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <!-- Spring Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- MyBatis -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>
        <!-- MySQL 驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- Seata -->
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
            <version>1.6.1</version>
        </dependency>
        <!-- Nacos 注册 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <version>2.2.7.RELEASE</version>
        </dependency>
        <!-- OpenFeign -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <version>2.2.9.RELEASE</version>
        </dependency>
    </dependencies>
</project>
2. 配置文件(application.yml)

yaml

server:
  port: 8081  # 订单服务端口

spring:
  application:
    name: order-service-tcc  # 服务名
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/order_db_tcc?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
    username: root
    password: 123456  # 替换为你的密码
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848

mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.example.order.entity

seata:
  enabled: true
  application-id: ${spring.application.name}
  tx-service-group: my_test_tx_group  # 事务组
  data-source-proxy-mode: NONE  # TCC 无需数据源代理(核心)
  service:
    vgroup-mapping:
      my_test_tx_group: default
    grouplist:
      default: 127.0.0.1:8091
  registry:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
3. 实体类与 Mapper
(1)订单实体(Order.java)

java

运行

package com.example.order.entity;

public class Order {
    private Long id;
    private String userId;
    private String productId;
    private Integer count;
    private String status;  // 状态:PENDING(待确认)、CONFIRMED(已确认)、CANCELED(已取消)

    // 构造器、getter、setter
    public Order() {}
    public Order(String userId, String productId, Integer count, String status) {
        this.userId = userId;
        this.productId = productId;
        this.count = count;
        this.status = status;
    }

    // getter 和 setter 省略
}
(2)OrderMapper.java

java

运行

package com.example.order.mapper;

import com.example.order.entity.Order;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface OrderMapper {
    void insert(Order order);  // 创建订单
    void updateStatus(@Param("id") Long id, @Param("status") String status);  // 更新订单状态
    Order selectById(@Param("id") Long id);  // 查询订单
}
(3)OrderMapper.xml(classpath:mapper/OrderMapper.xml)

xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.order.mapper.OrderMapper">
    <insert id="insert" parameterType="com.example.order.entity.Order">
        INSERT INTO `order` (user_id, product_id, count, status)
        VALUES (#{userId}, #{productId}, #{count}, #{status})
    </insert>

    <update id="updateStatus">
        UPDATE `order` SET status = #{status} WHERE id = #{id}
    </update>

    <select id="selectById" resultType="com.example.order.entity.Order">
        SELECT * FROM `order` WHERE id = #{id}
    </select>
</mapper>
4. TCC 接口与实现(核心)
(1)订单 TCC 接口(OrderTccService.java)

java

运行

package com.example.order.service;

import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;

// @LocalTCC 标记为 TCC 接口
@LocalTCC
public interface OrderTccService {

    // Try 阶段:创建待确认订单
    @TwoPhaseBusinessAction(
        name = "orderTcc",  // TCC 事务名称(唯一)
        commitMethod = "confirm",  // Confirm 阶段方法名
        rollbackMethod = "cancel"  // Cancel 阶段方法名
    )
    void tryCreateOrder(
        @BusinessActionContextParameter(paramName = "userId") String userId,
        @BusinessActionContextParameter(paramName = "productId") String productId,
        @BusinessActionContextParameter(paramName = "count") Integer count
    );

    // Confirm 阶段:确认订单(状态改为已完成)
    boolean confirm(BusinessActionContext context);

    // Cancel 阶段:取消订单(状态改为已取消)
    boolean cancel(BusinessActionContext context);
}
(2)订单 TCC 实现类(OrderTccServiceImpl.java)

java

运行

package com.example.order.service.impl;

import com.example.order.entity.Order;
import com.example.order.feign.InventoryTccFeignClient;
import com.example.order.mapper.OrderMapper;
import com.example.order.service.OrderTccService;
import io.seata.rm.tcc.api.BusinessActionContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderTccServiceImpl implements OrderTccService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private InventoryTccFeignClient inventoryTccFeignClient;  // 调用库存 TCC 接口

    // ThreadLocal 存储 Try 阶段创建的订单ID,供 Confirm/Cancel 使用
    private ThreadLocal<Long> orderIdHolder = new ThreadLocal<>();

    /**
     * Try 阶段:
     * 1. 创建状态为 PENDING 的订单(预留资源)
     * 2. 调用库存服务的 Try 阶段扣减库存
     */
    @Override
    @Transactional
    public void tryCreateOrder(String userId, String productId, Integer count) {
        // 1. 创建待确认订单
        Order order = new Order(userId, productId, count, "PENDING");
        orderMapper.insert(order);
        orderIdHolder.set(order.getId());  // 存储订单ID
        System.out.println("订单 Try 阶段成功:" + order.getId() + ",状态:PENDING");

        // 2. 调用库存服务的 Try 阶段(扣减可用库存,增加冻结库存)
        boolean inventoryTryResult = inventoryTccFeignClient.tryDeductStock(productId, count);
        if (!inventoryTryResult) {
            throw new RuntimeException("库存 Try 阶段失败,触发回滚");
        }
    }

    /**
     * Confirm 阶段:
     * 订单状态改为 CONFIRMED(确认提交)
     */
    @Override
    @Transactional
    public boolean confirm(BusinessActionContext context) {
        Long orderId = orderIdHolder.get();
        if (orderId == null) {
            // 从上下文获取订单ID(分布式场景下可能需要)
            orderId = Long.valueOf(context.getActionContext("orderId").toString());
        }
        orderMapper.updateStatus(orderId, "CONFIRMED");
        System.out.println("订单 Confirm 阶段成功:" + orderId + ",状态:CONFIRMED");
        return true;
    }

    /**
     * Cancel 阶段:
     * 订单状态改为 CANCELED(取消)
     */
    @Override
    @Transactional
    public boolean cancel(BusinessActionContext context) {
        Long orderId = orderIdHolder.get();
        if (orderId == null) {
            orderId = Long.valueOf(context.getActionContext("orderId").toString());
        }
        // 防止重复取消(查询当前状态)
        Order order = orderMapper.selectById(orderId);
        if (order != null && "PENDING".equals(order.getStatus())) {
            orderMapper.updateStatus(orderId, "CANCELED");
            System.out.println("订单 Cancel 阶段成功:" + orderId + ",状态:CANCELED");
        }
        return true;
    }
}
5. Feign 客户端(调用库存 TCC 接口)

java

运行

package com.example.order.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(name = "inventory-service-tcc")  // 库存服务名
public interface InventoryTccFeignClient {

    // 调用库存服务的 Try 阶段
    @PostMapping("/inventory/tryDeduct")
    boolean tryDeductStock(
        @RequestParam("productId") String productId,
        @RequestParam("count") Integer count
    );
}
6. 控制层(触发全局事务)

java

运行

package com.example.order.controller;

import com.example.order.service.OrderTccService;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;

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

    @Autowired
    private OrderTccService orderTccService;

    // 全局事务入口(TCC 模式仍需 @GlobalTransactional 标记)
    @PostMapping("/create")
    @GlobalTransactional(rollbackFor = Exception.class)
    public String createOrder(
            @RequestParam String userId,
            @RequestParam String productId,
            @RequestParam Integer count) {
        try {
            orderTccService.tryCreateOrder(userId, productId, count);
            return "订单创建成功(TCC 模式)";
        } catch (Exception e) {
            return "订单创建失败:" + e.getMessage();
        }
    }
}
7. 启动类

java

运行

package com.example.order;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients
public class OrderTccApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderTccApplication.class, args);
    }
}
8. 初始化订单库表(order_db_tcc)

sql

-- 订单表(TCC 需记录状态用于 Confirm/Cancel)
CREATE TABLE `order` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` varchar(32) NOT NULL,
  `product_id` varchar(32) NOT NULL,
  `count` int(11) NOT NULL,
  `status` varchar(32) DEFAULT NULL COMMENT 'PENDING/CONFIRMED/CANCELED',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

四、库存服务(inventory-service-tcc)

1. pom.xml 依赖

与订单服务一致。

2. 配置文件(application.yml)

yaml

server:
  port: 8082  # 库存服务端口

spring:
  application:
    name: inventory-service-tcc  # 服务名
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/inventory_db_tcc?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
    username: root
    password: 123456  # 替换为你的密码
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848

mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.example.inventory.entity

seata:
  enabled: true
  application-id: ${spring.application.name}
  tx-service-group: my_test_tx_group  # 同一事务组
  data-source-proxy-mode: NONE  # TCC 无需数据源代理
  service:
    vgroup-mapping:
      my_test_tx_group: default
    grouplist:
      default: 127.0.0.1:8091
  registry:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
3. 实体类与 Mapper
(1)库存实体(Inventory.java)

java

运行

package com.example.inventory.entity;

public class Inventory {
    private Long id;
    private String productId;
    private Integer stock;  // 可用库存
    private Integer frozenStock;  // 冻结库存(TCC 预留用)

    // getter 和 setter 省略
}
(2)InventoryMapper.java

java

运行

package com.example.inventory.mapper;

import com.example.inventory.entity.Inventory;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface InventoryMapper {
    Inventory selectByProductId(@Param("productId") String productId);  // 查询库存
    void updateStock(@Param("productId") String productId, @Param("stock") Integer stock, @Param("frozenStock") Integer frozenStock);  // 更新库存和冻结量
}
(3)InventoryMapper.xml(classpath:mapper/InventoryMapper.xml)

xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.inventory.mapper.InventoryMapper">
    <select id="selectByProductId" resultType="com.example.inventory.entity.Inventory">
        SELECT * FROM inventory WHERE product_id = #{productId}
    </select>

    <update id="updateStock">
        UPDATE inventory 
        SET stock = #{stock}, frozen_stock = #{frozenStock} 
        WHERE product_id = #{productId}
    </update>
</mapper>
4. TCC 接口与实现(核心)
(1)库存 TCC 接口(InventoryTccService.java)

java

运行

package com.example.inventory.service;

import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;

@LocalTCC
public interface InventoryTccService {

    // Try 阶段:扣减可用库存,增加冻结库存
    @TwoPhaseBusinessAction(
        name = "inventoryTcc",
        commitMethod = "confirm",
        rollbackMethod = "cancel"
    )
    boolean tryDeductStock(
        @BusinessActionContextParameter(paramName = "productId") String productId,
        @BusinessActionContextParameter(paramName = "count") Integer count
    );

    // Confirm 阶段:扣减冻结库存(确认扣减)
    boolean confirm(BusinessActionContext context);

    // Cancel 阶段:回滚可用库存,扣减冻结库存(取消预留)
    boolean cancel(BusinessActionContext context);
}
(2)库存 TCC 实现类(InventoryTccServiceImpl.java)

java

运行

package com.example.inventory.service.impl;

import com.example.inventory.entity.Inventory;
import com.example.inventory.mapper.InventoryMapper;
import com.example.inventory.service.InventoryTccService;
import io.seata.rm.tcc.api.BusinessActionContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class InventoryTccServiceImpl implements InventoryTccService {

    @Autowired
    private InventoryMapper inventoryMapper;

    /**
     * Try 阶段:
     * 1. 检查可用库存是否充足
     * 2. 扣减可用库存,增加冻结库存(预留资源)
     */
    @Override
    @Transactional
    public boolean tryDeductStock(String productId, Integer count) {
        Inventory inventory = inventoryMapper.selectByProductId(productId);
        if (inventory == null) {
            throw new RuntimeException("商品不存在");
        }
        // 检查可用库存
        if (inventory.getStock() < count) {
            throw new RuntimeException("库存不足");
        }
        // 扣减可用库存,增加冻结库存
        int newStock = inventory.getStock() - count;
        int newFrozenStock = inventory.getFrozenStock() + count;
        inventoryMapper.updateStock(productId, newStock, newFrozenStock);
        System.out.println("库存 Try 阶段成功:" + productId + ",可用库存:" + newStock + ",冻结库存:" + newFrozenStock);
        return true;
    }

    /**
     * Confirm 阶段:
     * 扣减冻结库存(确认资源扣减)
     */
    @Override
    @Transactional
    public boolean confirm(BusinessActionContext context) {
        String productId = context.getActionContext("productId").toString();
        Integer count = (Integer) context.getActionContext("count");

        Inventory inventory = inventoryMapper.selectByProductId(productId);
        if (inventory == null) {
            return false;
        }
        // 扣减冻结库存
        int newFrozenStock = inventory.getFrozenStock() - count;
        inventoryMapper.updateStock(productId, inventory.getStock(), newFrozenStock);
        System.out.println("库存 Confirm 阶段成功:" + productId + ",冻结库存扣减:" + count);
        return true;
    }

    /**
     * Cancel 阶段:
     * 1. 回滚可用库存(恢复被扣减的部分)
     * 2. 扣减冻结库存(取消预留)
     */
    @Override
    @Transactional
    public boolean cancel(BusinessActionContext context) {
        String productId = context.getActionContext("productId").toString();
        Integer count = (Integer) context.getActionContext("count");

        Inventory inventory = inventoryMapper.selectByProductId(productId);
        if (inventory == null) {
            return false;
        }
        // 回滚可用库存,扣减冻结库存
        int newStock = inventory.getStock() + count;
        int newFrozenStock = inventory.getFrozenStock() - count;
        inventoryMapper.updateStock(productId, newStock, newFrozenStock);
        System.out.println("库存 Cancel 阶段成功:" + productId + ",可用库存回滚:" + count + ",冻结库存扣减:" + count);
        return true;
    }
}
5. 控制层(暴露 TCC 接口)

java

运行

package com.example.inventory.controller;

import com.example.inventory.service.InventoryTccService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;

@RestController
@RequestMapping("/inventory")
public class InventoryController {

    @Autowired
    private InventoryTccService inventoryTccService;

    // 暴露 Try 阶段接口给订单服务调用
    @PostMapping("/tryDeduct")
    public boolean tryDeductStock(
            @RequestParam("productId") String productId,
            @RequestParam("count") Integer count) {
        return inventoryTccService.tryDeductStock(productId, count);
    }
}
6. 启动类

java

运行

package com.example.inventory;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class InventoryTccApplication {
    public static void main(String[] args) {
        SpringApplication.run(InventoryTccApplication.class, args);
    }
}
7. 初始化库存库表(inventory_db_tcc)

sql

-- 库存表(TCC 需冻结库存字段)
CREATE TABLE `inventory` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `product_id` varchar(32) NOT NULL,
  `stock` int(11) NOT NULL COMMENT '可用库存',
  `frozen_stock` int(11) DEFAULT 0 COMMENT '冻结库存(TCC 预留用)',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_product_id` (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 初始化库存数据
INSERT INTO `inventory` (`product_id`, `stock`, `frozen_stock`) VALUES ('P001', 100, 0);

五、测试流程

  1. 启动 Nacos 和 Seata Server。
  2. 启动库存服务和订单服务。
  3. 调用订单接口:POST http://localhost:8081/order/create?userId=U001&productId=P001&count=10
  4. 正常情况:
    • 订单状态变为 CONFIRMED
    • 库存表 P001 可用库存减少 10,冻结库存不变(Confirm 阶段扣减了冻结库存)。
  5. 测试回滚:在订单服务 tryCreateOrder 方法中手动抛出异常(如 throw new RuntimeException("测试回滚")),再次调用接口:
    • 订单状态变为 CANCELED
    • 库存表可用库存恢复原值,冻结库存为 0(Cancel 阶段回滚)。

TCC 模式核心说明

  1. 三阶段方法:必须实现 Try(资源预留)、Confirm(确认提交)、Cancel(补偿回滚),通过 @TwoPhaseBusinessAction 关联。
  2. 无数据源代理:配置 data-source-proxy-mode: NONE,无需 undo_log 表,依赖业务代码实现补偿。
  3. 状态管理:需在业务表中记录中间状态(如订单 PENDING、库存 frozen_stock),用于 Confirm/Cancel 阶段判断。
  4. 幂等性设计:Confirm/Cancel 方法需支持重复调用(如通过状态判断避免重复执行)。

TCC 模式性能优于 AT/XA,但代码侵入性高,适合高并发场景(如秒杀)或非 SQL 操作(如缓存、消息队列)。

---TCC 注意:

@BusinessActionContextParameter 是 Seata TCC 模式中的核心注解,用于 将方法参数存入全局事务上下文(BusinessActionContext,以便在 Confirm 或 Cancel 阶段获取这些参数,实现分布式事务的补偿逻辑。

一、核心作用

在 TCC 模式中,Try 阶段的参数(如商品 ID、数量、订单 ID 等)往往需要在后续的 Confirm(确认)或 Cancel(取消)阶段使用(例如:确认扣减库存时需要知道扣减数量,取消订单时需要知道订单 ID)。

@BusinessActionContextParameter 的作用就是:在 Try 方法执行时,自动将标记的参数保存到 BusinessActionContext 中,供 Confirm/Cancel 方法通过上下文获取,避免因分布式环境下参数传递丢失而导致补偿逻辑失败。

二、使用场景

以 TCC 模式的 “下单扣库存” 为例:

  1. Try 阶段需要传递 productId(商品 ID)和 count(扣减数量);
  2. Confirm 阶段需要用这两个参数完成最终库存扣减;
  3. Cancel 阶段需要用这两个参数回滚库存。

通过 @BusinessActionContextParameter 标记这两个参数,即可在 Confirm/Cancel 阶段从上下文获取。

三、代码示例

1. Try 阶段标记参数

java

运行

@LocalTCC
public interface InventoryTccService {
    // Try 阶段:扣减可用库存,增加冻结库存
    @TwoPhaseBusinessAction(
        name = "inventoryTcc",
        commitMethod = "confirm",  // 关联 Confirm 方法
        rollbackMethod = "cancel"  // 关联 Cancel 方法
    )
    boolean tryDeductStock(
        // 标记 productId,存入上下文
        @BusinessActionContextParameter(paramName = "productId") String productId,
        // 标记 count,存入上下文
        @BusinessActionContextParameter(paramName = "count") Integer count
    );

    // Confirm 阶段:使用上下文参数完成扣减
    boolean confirm(BusinessActionContext context);

    // Cancel 阶段:使用上下文参数回滚
    boolean cancel(BusinessActionContext context);
}
2. Confirm/Cancel 阶段从上下文获取参数

java

运行

@Override
public boolean confirm(BusinessActionContext context) {
    // 从上下文获取 Try 阶段标记的 productId
    String productId = context.getActionContext("productId").toString();
    // 从上下文获取 Try 阶段标记的 count
    Integer count = (Integer) context.getActionContext("count");

    // 执行确认逻辑:扣减冻结库存
    Inventory inventory = inventoryMapper.selectByProductId(productId);
    int newFrozenStock = inventory.getFrozenStock() - count;
    inventoryMapper.updateStock(productId, inventory.getStock(), newFrozenStock);
    return true;
}

@Override
public boolean cancel(BusinessActionContext context) {
    // 同样从上下文获取参数
    String productId = context.getActionContext("productId").toString();
    Integer count = (Integer) context.getActionContext("count");

    // 执行回滚逻辑:恢复可用库存,扣减冻结库存
    Inventory inventory = inventoryMapper.selectByProductId(productId);
    int newStock = inventory.getStock() + count;
    int newFrozenStock = inventory.getFrozenStock() - count;
    inventoryMapper.updateStock(productId, newStock, newFrozenStock);
    return true;
}

四、关键细节

  1. paramName 属性:用于指定参数在上下文(BusinessActionContext)中的 key,后续通过 context.getActionContext(key) 获取。若不指定,默认使用参数名作为 key。

  2. 参数类型支持:支持基本类型(StringIntegerLong 等)和序列化对象(需实现 Serializable),Seata 会自动将参数序列化后存入上下文。

  3. 分布式场景必要性:在微服务调用中,Try 阶段的参数可能跨服务传递,Confirm/Cancel 阶段可能在不同的服务实例中执行,通过上下文存储参数可确保补偿逻辑能正确获取所需数据。

  4. 与 BusinessActionContext 的关系BusinessActionContext 是 TCC 事务的全局上下文,包含事务 ID(xid)、分支 ID(branchId)以及 @BusinessActionContextParameter 标记的参数,由 Seata 自动管理生命周期。

总结

@BusinessActionContextParameter 是 TCC 模式中连接 Try 阶段与 Confirm/Cancel 阶段的关键注解,通过将参数存入全局上下文,确保分布式环境下补偿逻辑能正确获取所需数据,是实现可靠 TCC 事务的核心机制之一。

在 Seata AT 模式下,事务执行成功后,undo_log 表中的对应记录会被删除;若执行失败(触发回滚),undo_log 表中的记录会被用于回滚操作,回滚完成后也会被删除。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值