SpringBoot Seata TCC最佳实践

437 阅读3分钟

以下是在 Spring Boot 中整合 Seata TCC 模式、PostgreSQL 和 MyBatis 实现分布式事务的完整配置和示例:


1. TCC 模式核心概念

  • Try:预留资源(如冻结库存)。
  • Confirm:确认操作(如实际扣减库存)。
  • Cancel:补偿操作(如释放冻结的库存)。

2. 项目结构

src/
├── main/
│   ├── java/
│   │   └── com/example/
│   │       ├── tcc/               # TCC 接口定义
│   │       │   ├── InventoryTccService.java
│   │       │   └── OrderTccService.java
│   │       ├── mapper/            # MyBatis Mapper接口
│   │       │   ├── InventoryMapper.java
│   │       │   └── OrderMapper.java
│   │       ├── model/             # 实体类
│   │       │   ├── Inventory.java
│   │       │   └── Order.java
│   │       ├── service/           # 服务层
│   │       │   └── OrderService.java
│   │       └── Application.java   # 启动类
│   └── resources/
│       ├── mapper/                # MyBatis XML
│       │   ├── InventoryMapper.xml
│       │   └── OrderMapper.xml
│       ├── application.yml        # 配置文件
│       └── seata.conf            # Seata 配置(可选)

3. 依赖配置(pom.xml)

<dependencies>
    <!-- Spring Boot Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- PostgreSQL 驱动 -->
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <version>42.6.0</version>
    </dependency>
    
    <!-- MyBatis -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.0</version>
    </dependency>
    
    <!-- Seata -->
    <dependency>
        <groupId>io.seata</groupId>
        <artifactId>seata-spring-boot-starter</artifactId>
        <version>1.5.2</version>
    </dependency>
    
    <!-- OpenFeign(用于跨服务调用) -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
        <version>3.1.0</version>
    </dependency>
</dependencies>

4. Seata Server 配置

  1. 下载并启动 Seata Server(参考前文)。
  2. 配置注册中心和配置中心(如 Nacos)。

5. Spring Boot 应用配置

(1) application.yml

spring:
  application:
    name: order-service
  datasource:
    url: jdbc:postgresql://localhost:5432/order_db
    username: postgres
    password: 123456
    driver-class-name: org.postgresql.Driver

# Seata 配置
seata:
  enabled: true
  application-id: order-service
  tx-service-group: my-tx-group
  service:
    vgroup-mapping:
      my-tx-group: default
    grouplist:
      default: 127.0.0.1:8091
  registry:
    type: nacos
    nacos:
      server-addr: localhost:8848
  config:
    type: nacos

# OpenFeign 配置
feign:
  client:
    config:
      default:
        connectTimeout: 5000
        readTimeout: 5000

6. TCC 接口定义与实现

(1) 库存服务 TCC 接口

@LocalTCC
public interface InventoryTccService {
​
    @TwoPhaseBusinessAction(name = "deductInventory", commitMethod = "confirm", rollbackMethod = "cancel")
    boolean tryDeduct(@BusinessActionContextParameter(paramName = "productId") Long productId,
                     @BusinessActionContextParameter(paramName = "count") Integer count);
​
    boolean confirm(BusinessActionContext context);
    boolean cancel(BusinessActionContext context);
}

(2) 库存服务 TCC 实现

@Service
public class InventoryTccServiceImpl implements InventoryTccService {
​
    @Autowired
    private InventoryMapper inventoryMapper;
​
    @Override
    public boolean tryDeduct(Long productId, Integer count) {
        // Try 阶段:冻结库存
        inventoryMapper.freezeInventory(productId, count);
        return true;
    }
​
    @Override
    public boolean confirm(BusinessActionContext context) {
        // Confirm 阶段:实际扣减库存
        Long productId = (Long) context.getActionContext("productId");
        Integer count = (Integer) context.getActionContext("count");
        inventoryMapper.reduceInventory(productId, count);
        return true;
    }
​
    @Override
    public boolean cancel(BusinessActionContext context) {
        // Cancel 阶段:释放冻结的库存
        Long productId = (Long) context.getActionContext("productId");
        Integer count = (Integer) context.getActionContext("count");
        inventoryMapper.unfreezeInventory(productId, count);
        return true;
    }
}

7. MyBatis Mapper 与实体类

(1) 实体类 Inventory.java

public class Inventory {
    private Long id;
    private Long productId;
    private Integer total;
    private Integer frozen;
    // Getters/Setters
}

(2) Mapper 接口 InventoryMapper.java

@Mapper
public interface InventoryMapper {
    @Update("UPDATE inventory SET frozen = frozen + #{count} WHERE product_id = #{productId}")
    void freezeInventory(@Param("productId") Long productId, @Param("count") Integer count);
​
    @Update("UPDATE inventory SET total = total - #{count}, frozen = frozen - #{count} WHERE product_id = #{productId}")
    void reduceInventory(@Param("productId") Long productId, @Param("count") Integer count);
​
    @Update("UPDATE inventory SET frozen = frozen - #{count} WHERE product_id = #{productId}")
    void unfreezeInventory(@Param("productId") Long productId, @Param("count") Integer count);
}

8. 服务层与全局事务

(1) 订单服务(主事务发起方)

@Service
public class OrderService {
​
    @Autowired
    private InventoryTccService inventoryTccService;
​
    @GlobalTransactional(name = "createOrder", rollbackFor = Exception.class)
    public void createOrder(Order order) {
        // TCC Try 阶段:冻结库存
        inventoryTccService.tryDeduct(order.getProductId(), order.getCount());
        
        // 本地事务:创建订单
        orderMapper.createOrder(order);
    }
}

(2) MyBatis Mapper OrderMapper.java

@Mapper
public interface OrderMapper {
    @Insert("INSERT INTO orders(product_id, count, status) VALUES(#{productId}, #{count}, 'CREATED')")
    void createOrder(Order order);
}

9. PostgreSQL 表结构

(1) 库存表 inventory

CREATE TABLE inventory (
  id SERIAL PRIMARY KEY,
  product_id BIGINT UNIQUE NOT NULL,
  total INT NOT NULL,
  frozen INT NOT NULL DEFAULT 0
);

(2) 订单表 orders

CREATE TABLE orders (
  id SERIAL PRIMARY KEY,
  product_id BIGINT NOT NULL,
  count INT NOT NULL,
  status VARCHAR(20)
);

10. 测试与验证

  1. 初始化库存数据

    INSERT INTO inventory (product_id, total) VALUES (1001, 100);
    
  2. 执行下单操作

    @SpringBootTest
    public class OrderServiceTest {
    
        @Autowired
        private OrderService orderService;
    
        @Test
        public void testTccSuccess() {
            Order order = new Order();
            order.setProductId(1001L);
            order.setCount(10);
            orderService.createOrder(order);
            // 验证库存 total=90,frozen=0
        }
    
        @Test
        public void testTccRollback() {
            Order order = new Order();
            order.setProductId(1001L);
            order.setCount(10);
            try {
                orderService.createOrder(order);
                throw new RuntimeException("模拟异常");
            } catch (Exception e) {
                // 验证库存 total=100,frozen=0
            }
        }
    }
    

11. 关键点总结

  1. TCC 接口定义

    • 使用 @LocalTCC@TwoPhaseBusinessAction 注解定义 TCC 接口。
    • 通过 @BusinessActionContextParameter 传递参数到全局上下文。
  2. 数据源配置

    • TCC 模式不依赖 DataSourceProxy,但需确保本地事务与 TCC 协调一致。
  3. 幂等性与空回滚

    • 实现 TCC 时需处理幂等性(多次调用 Confirm/Cancel 不重复扣减)。
    • 处理空回滚(Try 未执行但 Cancel 被调用)。
  4. 跨服务调用

    • 若 TCC 服务跨微服务,需通过 Feign 调用并传递 XID

总结

通过以上配置,可以实现:

  1. TCC 事务管理:通过自定义 Try/Confirm/Cancel 方法实现柔性事务。
  2. PostgreSQL 支持:结合 MyBatis 操作数据库。
  3. 分布式事务协调:Seata 保证跨服务操作的最终一致性。