SpringBoot Seata Saga 最佳实践

242 阅读3分钟

以下是在 Spring Boot 中整合 Seata Saga 模式、PostgreSQL 和 MyBatis 的完整配置和示例:


1. 项目结构

src/
├── main/
│   ├── java/
│   │   └── com/example/
│   │       ├── saga/              # Saga 状态机定义
│   │       │   └── OrderSaga.java
│   │       ├── service/           # 服务层
│   │       │   ├── InventoryService.java
│   │       │   └── OrderService.java
│   │       ├── mapper/            # MyBatis Mapper接口
│   │       │   ├── InventoryMapper.java
│   │       │   └── OrderMapper.java
│   │       ├── model/             # 实体类
│   │       │   ├── Inventory.java
│   │       │   └── Order.java
│   │       └── Application.java   # 启动类
│   └── resources/
│       ├── statemachine/          # Saga 状态机配置
│       │   └── order_saga.json
│       ├── mapper/                # MyBatis XML
│       │   ├── InventoryMapper.xml
│       │   └── OrderMapper.xml
│       ├── application.yml        # 配置文件
│       └── seata.conf            # Seata 配置(可选)

2. 依赖配置(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 Saga -->
    <dependency>
        <groupId>io.seata</groupId>
        <artifactId>seata-spring-boot-starter</artifactId>
        <version>1.5.2</version>
    </dependency>
</dependencies>

3. Seata Server 配置

  1. 下载并启动 Seata Server

    • Seata GitHub Release 下载并解压。

    • 修改 conf/registry.conf,配置注册中心(如 Nacos)和配置中心:

      registry {
        type = "nacos"
        nacos {
          serverAddr = "localhost:8848"
          namespace = ""
          cluster = "default"
        }
      }
      config {
        type = "nacos"
        nacos {
          serverAddr = "localhost:8848"
          namespace = ""
          group = "SEATA_GROUP"
        }
      }
      
    • 启动 Seata Server:

      sh bin/seata-server.sh -p 8091 -h 0.0.0.0
      

4. 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

5. Saga 状态机定义

(1) 状态机 JSON 配置(statemachine/order_saga.json

{
  "Name": "createOrderSaga",
  "Comment": "创建订单的 Saga 流程",
  "StartState": "CreateOrder",
  "States": {
    "CreateOrder": {
      "ServiceName": "orderService",
      "ServiceMethod": "createOrder",
      "CompensateState": "CompensateCreateOrder",
      "Next": "DeductInventory"
    },
    "DeductInventory": {
      "ServiceName": "inventoryService",
      "ServiceMethod": "deductInventory",
      "CompensateState": "CompensateDeductInventory",
      "IsEnd": true
    },
    "CompensateCreateOrder": {
      "ServiceName": "orderService",
      "ServiceMethod": "compensateCreateOrder",
      "IsEnd": true
    },
    "CompensateDeductInventory": {
      "ServiceName": "inventoryService",
      "ServiceMethod": "compensateDeductInventory",
      "IsEnd": true
    }
  }
}

(2) 加载状态机配置

@Configuration
public class SagaConfig {
​
    @Bean
    public StateMachineEngine stateMachineEngine(SeataSagaClient seataSagaClient) {
        return new StateMachineEngine(seataSagaClient)
            .withStateMachineRepository(new ResourceStateMachineRepository())
            .addStateMachineConfig("classpath:statemachine/order_saga.json");
    }
}

6. 服务层实现

(1) 订单服务(正向操作 + 补偿)

@Service
public class OrderService {
​
    @Autowired
    private OrderMapper orderMapper;
​
    @SagaStart(timeoutSeconds = 60) // 启动 Saga 事务
    public void createOrder(Order order) {
        orderMapper.createOrder(order);
        System.out.println("订单创建成功:" + order.getId());
    }
​
    public void compensateCreateOrder(Order order) {
        orderMapper.deleteOrder(order.getId());
        System.out.println("补偿:订单已删除:" + order.getId());
    }
}

(2) 库存服务(正向操作 + 补偿)

@Service
public class InventoryService {
​
    @Autowired
    private InventoryMapper inventoryMapper;
​
    public void deductInventory(Long productId, Integer count) {
        inventoryMapper.deductInventory(productId, count);
        System.out.println("库存扣减成功:" + productId + ", 数量:" + count);
    }
​
    public void compensateDeductInventory(Long productId, Integer count) {
        inventoryMapper.addInventory(productId, count);
        System.out.println("补偿:库存已恢复:" + productId + ", 数量:" + count);
    }
}

7. MyBatis Mapper 与实体类

(1) 订单实体类 Order.java

public class Order {
    private Long id;
    private Long productId;
    private Integer count;
    private String status;
    // Getters/Setters
}

(2) 订单 Mapper 接口 OrderMapper.java

@Mapper
public interface OrderMapper {
    @Insert("INSERT INTO orders(product_id, count, status) VALUES(#{productId}, #{count}, 'CREATED')")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    void createOrder(Order order);
​
    @Delete("DELETE FROM orders WHERE id = #{id}")
    void deleteOrder(Long id);
}

(3) 库存实体类 Inventory.java

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

(4) 库存 Mapper 接口 InventoryMapper.java

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

8. PostgreSQL 表结构

(1) 订单表 orders

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

(2) 库存表 inventory

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

9. 测试与验证

(1) 正常流程测试

@SpringBootTest
public class OrderServiceTest {
​
    @Autowired
    private OrderService orderService;
​
    @Test
    public void testSagaSuccess() {
        Order order = new Order();
        order.setProductId(1001L);
        order.setCount(10);
        orderService.createOrder(order); // 触发 Saga
        // 验证订单和库存均更新成功
    }
}

(2) 补偿流程测试

@SpringBootTest
public class OrderServiceTest {
​
    @Autowired
    private OrderService orderService;
​
    @Test
    public void testSagaRollback() {
        Order order = new Order();
        order.setProductId(1001L);
        order.setCount(10);
        try {
            orderService.createOrder(order);
            throw new RuntimeException("模拟库存扣减失败"); // 触发补偿
        } catch (Exception e) {
            // 验证订单被删除,库存恢复
        }
    }
}

10. 关键注意事项

  1. 幂等性处理

    • 确保补偿操作可重复执行(如删除订单前检查是否存在)。
    • 在数据库操作中增加版本号或状态字段。
  2. 空回滚处理

    • 若正向操作未执行但补偿被触发,需跳过无效操作。
  3. 状态机配置

    • 确保 JSON 文件路径正确,状态名称与代码一致。
    • 使用 @SagaStart 注解触发事务。

总结

通过以上步骤,Spring Boot 应用能够:

  1. 实现 Saga 事务:通过状态机定义长事务流程。
  2. 整合 PostgreSQL 和 MyBatis:确保数据操作正确。
  3. 自动补偿机制:在异常情况下回滚已执行操作。

此方案适用于需要最终一致性的分布式系统,尤其在微服务架构中处理复杂业务流程时表现出色。