┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ TM │ │ TC │ │ RM │
│ (订单服务) │ │ (Seata Server) │ (库存服务) │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
│ 1. 开启全局事务 │ │
├──────────────────────>│ │
│ │ │
│ 2. 返回 xid │ │
│<──────────────────────┤ │
│ │ │
│ 3. 调用 RM(携带 xid) │ │
├──────────────────────────────────────────────>│
│ │ │
│ │ 4. 注册分支事务 │
│ │<──────────────────────┤
│ │ │
│ │ 5. 执行一阶段 + 反馈结果│
│ │<──────────────────────┤
│ │ │
│ │ 6. 判断全局状态(提交/回滚)│
│ │(内部逻辑) │
│ │ │
│ │ 7. 发送二阶段指令 │
│ ├──────────────────────>│
│ │ │
│ │ 8. 反馈二阶段结果 │
│ │<──────────────────────┤
│ │ │
│ 9. 通知 TM 最终结果 │ │
│<──────────────────────┤ │
│ │ │
| 模式 | 核心配置 / 注解 | 数据源代理 | 依赖表 | 配置文件指定模式 |
|---|---|---|---|---|
| AT | @GlobalTransactional | DataSourceProxy | undo_log | 无需(默认适配)或者seata.data-source-proxy-mode: AT |
| TCC | @TwoPhaseBusinessAction | 无需代理(原始数据源) | 无(手动补偿) | 无需(通过注解识别) |
| 2PC(XA) | @GlobalTransactional | DataSourceProxyXA | 无(依赖数据库 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.properties,Group=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;
五、测试流程
- 启动 Nacos:
startup.sh -m standalone - 启动 Seata Server(按步骤二)
- 启动库存服务和订单服务
- 调用订单创建接口:
POST http://localhost:8081/order/create?userId=U001&productId=P001&count=10 - 正常情况:订单表新增记录,库存表
P001库存减少 10 - 测试回滚:取消订单服务
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 模式,包含 id、userId、productId、count、status 字段。
(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)
包含 id、productId、stock 字段。
(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);
五、测试流程
- 启动 Nacos 和 Seata Server(同 AT 模式)。
- 启动库存服务和订单服务。
- 调用订单接口:
POST http://localhost:8081/order/create?userId=U001&productId=P001&count=10 - 正常情况:订单表新增记录,库存表
P001库存减少 10。 - 测试回滚:取消订单服务中异常代码的注释,再次调用,观察订单和库存是否回滚(依赖 MySQL XA 协议回滚)。
XA 模式核心配置说明
- 数据源代理:必须使用
MysqlXADataSource(MySQL XA 驱动)和DataSourceProxyXA(Seata XA 代理)。 - 显式模式指定:
data-source-proxy-mode: XA是 XA 模式的关键标识,与 AT 模式区分。 - 无需 undo_log 表:XA 模式依赖数据库自身的 XA 协议回滚,不需要
undo_log表。 - 全局事务注解:仍使用
@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);
五、测试流程
- 启动 Nacos 和 Seata Server。
- 启动库存服务和订单服务。
- 调用订单接口:
POST http://localhost:8081/order/create?userId=U001&productId=P001&count=10 - 正常情况:
- 订单状态变为
CONFIRMED; - 库存表
P001可用库存减少 10,冻结库存不变(Confirm 阶段扣减了冻结库存)。
- 订单状态变为
- 测试回滚:在订单服务
tryCreateOrder方法中手动抛出异常(如throw new RuntimeException("测试回滚")),再次调用接口:- 订单状态变为
CANCELED; - 库存表可用库存恢复原值,冻结库存为 0(Cancel 阶段回滚)。
- 订单状态变为
TCC 模式核心说明
- 三阶段方法:必须实现
Try(资源预留)、Confirm(确认提交)、Cancel(补偿回滚),通过@TwoPhaseBusinessAction关联。 - 无数据源代理:配置
data-source-proxy-mode: NONE,无需undo_log表,依赖业务代码实现补偿。 - 状态管理:需在业务表中记录中间状态(如订单
PENDING、库存frozen_stock),用于 Confirm/Cancel 阶段判断。 - 幂等性设计: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 模式的 “下单扣库存” 为例:
Try阶段需要传递productId(商品 ID)和count(扣减数量);Confirm阶段需要用这两个参数完成最终库存扣减;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;
}
四、关键细节
-
paramName属性:用于指定参数在上下文(BusinessActionContext)中的 key,后续通过context.getActionContext(key)获取。若不指定,默认使用参数名作为 key。 -
参数类型支持:支持基本类型(
String、Integer、Long等)和序列化对象(需实现Serializable),Seata 会自动将参数序列化后存入上下文。 -
分布式场景必要性:在微服务调用中,
Try阶段的参数可能跨服务传递,Confirm/Cancel阶段可能在不同的服务实例中执行,通过上下文存储参数可确保补偿逻辑能正确获取所需数据。 -
与
BusinessActionContext的关系:BusinessActionContext是 TCC 事务的全局上下文,包含事务 ID(xid)、分支 ID(branchId)以及@BusinessActionContextParameter标记的参数,由 Seata 自动管理生命周期。
总结
@BusinessActionContextParameter 是 TCC 模式中连接 Try 阶段与 Confirm/Cancel 阶段的关键注解,通过将参数存入全局上下文,确保分布式环境下补偿逻辑能正确获取所需数据,是实现可靠 TCC 事务的核心机制之一。
在 Seata AT 模式下,事务执行成功后,undo_log 表中的对应记录会被删除;若执行失败(触发回滚),undo_log 表中的记录会被用于回滚操作,回滚完成后也会被删除。
4938

被折叠的 条评论
为什么被折叠?



