Seata AT模式详细实例:电商下单场景

Seata AT模式详细实例:电商下单场景

1. Seata AT模式核心原理

1.1 核心概念

  • AT模式:Automatic Transaction(自动事务),非侵入式的分布式事务解决方案
  • TC(Transaction Coordinator):事务协调器,管理全局事务状态
  • TM(Transaction Manager):事务管理器,发起和结束全局事务
  • RM(Resource Manager):资源管理器,管理分支事务,与TC交互
  • 全局事务ID(XID):唯一标识一个全局事务
  • Undo Log:回滚日志,用于事务回滚

1.2 工作流程

  1. 全局事务开启:TM向TC请求开启全局事务,获取XID
  2. 分支事务注册:RM向TC注册分支事务,关联XID
  3. SQL执行:RM执行SQL,生成Undo Log并写入数据库
  4. 分支事务提交:RM向TC报告分支事务状态
  5. 全局事务提交/回滚:TC根据分支事务状态,决定全局提交或回滚
  6. 分支事务提交/回滚:TC通知RM执行分支事务的提交或回滚

2. 项目结构设计

2.1 业务场景

电商下单流程

  • 创建订单(订单服务)
  • 扣减库存(库存服务)
  • 扣减账户余额(账户服务)

2.2 项目结构

复制代码
seata-at-demo/          # 父项目
├── order-service/      # 订单服务(8081)
├── inventory-service/  # 库存服务(8082)
└── account-service/    # 账户服务(8083)

3. 环境准备

3.1 组件版本

组件 版本
Spring Boot 2.7.18
Spring Cloud 2021.0.8
Seata 1.7.1
MySQL 8.0+
Nacos 2.2.3

3.2 数据库初始化

3.2.1 创建Seata所需数据库和表
sql 复制代码
-- 创建seata数据库
CREATE DATABASE seata;
USE seata;

-- 创建undo_log表(Seata AT模式必须)
CREATE TABLE undo_log (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    branch_id BIGINT NOT NULL,
    xid VARCHAR(100) NOT NULL,
    context VARCHAR(128) NOT NULL,
    rollback_info LONGBLOB NOT NULL,
    log_status INT NOT NULL,
    log_created DATETIME NOT NULL,
    log_modified DATETIME NOT NULL,
    UNIQUE KEY ux_undo_log (xid, branch_id)
);
3.2.2 业务数据库初始化

order-service数据库

sql 复制代码
CREATE DATABASE order_db;
USE order_db;

CREATE TABLE orders (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    user_id BIGINT NOT NULL,
    product_id BIGINT NOT NULL,
    count INT NOT NULL,
    money DECIMAL(10,2) NOT NULL,
    status INT DEFAULT 0 COMMENT '0:创建中,1:已创建,2:已取消'
);

CREATE TABLE undo_log (
    -- 同上,每个业务库都需要undo_log表
);

inventory-service数据库

sql 复制代码
CREATE DATABASE inventory_db;
USE inventory_db;

CREATE TABLE inventory (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    product_id BIGINT NOT NULL,
    count INT NOT NULL DEFAULT 0
);

INSERT INTO inventory (product_id, count) VALUES (1, 100);

CREATE TABLE undo_log (
    -- 同上
);

account-service数据库

sql 复制代码
CREATE DATABASE account_db;
USE account_db;

CREATE TABLE account (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    user_id BIGINT NOT NULL,
    money DECIMAL(10,2) NOT NULL DEFAULT 0.00
);

INSERT INTO account (user_id, money) VALUES (1, 1000.00);

CREATE TABLE undo_log (
    -- 同上
);

4. 代码实现

4.1 父项目POM.xml

xml 复制代码
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.18</version>
</parent>

<properties>
    <spring-cloud.version>2021.0.8</spring-cloud.version>
    <seata.version>1.7.1</seata.version>
</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
            <version>${seata.version}</version>
        </dependency>
    </dependencies>
</dependencyManagement>

4.2 订单服务实现

4.2.1 POM.xml
xml 复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
</dependencies>
4.2.2 application.yml
yaml 复制代码
server:
  port: 8081

spring:
  application:
    name: order-service
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/order_db?useSSL=false&serverTimezone=UTC
    username: root
    password: root
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    openfeign:
      client:
        config:
          default:
            connectTimeout: 5000
            readTimeout: 5000
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true

seata:
  enabled: true
  tx-service-group: my_test_tx_group
  registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: localhost:8848
      group: SEATA_GROUP
  config:
    type: nacos
    nacos:
      server-addr: localhost:8848
      group: SEATA_GROUP
  data-source-proxy-mode: AT
4.2.3 订单实体
java 复制代码
@Entity
@Table(name = "orders")
@Data
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private Long userId;
    private Long productId;
    private Integer count;
    private BigDecimal money;
    private Integer status = 0;
}
4.2.4 Feign客户端
java 复制代码
@FeignClient(name = "inventory-service")
public interface InventoryFeignClient {
    @PostMapping("/inventory/deduct")
    String deduct(@RequestParam Long productId, @RequestParam Integer count);
}

@FeignClient(name = "account-service")
public interface AccountFeignClient {
    @PostMapping("/account/deduct")
    String deduct(@RequestParam Long userId, @RequestParam BigDecimal money);
}
4.2.5 订单服务实现(核心)
java 复制代码
@Service
public class OrderService {
    
    @Autowired
    private OrderRepository orderRepository;
    
    @Autowired
    private InventoryFeignClient inventoryFeignClient;
    
    @Autowired
    private AccountFeignClient accountFeignClient;
    
    /**
     * 创建订单,Seata AT模式分布式事务
     * @GlobalTransactional:标记为全局事务
     */
    @GlobalTransactional(name = "create-order-tx", rollbackFor = Exception.class)
    public String createOrder(Order order) {
        System.out.println("开始创建订单...");
        
        // 1. 创建订单
        orderRepository.save(order);
        System.out.println("订单创建成功:" + order.getId());
        
        // 2. 扣减库存
        String inventoryResult = inventoryFeignClient.deduct(order.getProductId(), order.getCount());
        System.out.println("库存扣减结果:" + inventoryResult);
        
        // 3. 扣减账户余额
        BigDecimal totalMoney = order.getMoney().multiply(new BigDecimal(order.getCount()));
        String accountResult = accountFeignClient.deduct(order.getUserId(), totalMoney);
        System.out.println("账户扣减结果:" + accountResult);
        
        // 模拟异常,测试回滚
        // if (true) throw new RuntimeException("模拟异常,测试回滚");
        
        System.out.println("订单创建完成!");
        return "订单创建成功:" + order.getId();
    }
}
4.2.6 订单控制器
java 复制代码
@RestController
@RequestMapping("/order")
public class OrderController {
    
    @Autowired
    private OrderService orderService;
    
    @PostMapping("/create")
    public String createOrder(@RequestBody Order order) {
        return orderService.createOrder(order);
    }
}

4.3 库存服务实现

4.3.1 application.yml
yaml 复制代码
server:
  port: 8082

spring:
  application:
    name: inventory-service
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/inventory_db?useSSL=false&serverTimezone=UTC
    username: root
    password: root
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true

seata:
  enabled: true
  tx-service-group: my_test_tx_group
  # 其他配置同order-service
4.3.2 库存服务实现
java 复制代码
@Service
public class InventoryService {
    
    @Autowired
    private InventoryRepository inventoryRepository;
    
    /**
     * 扣减库存,分支事务
     */
    @Transactional(rollbackFor = Exception.class)
    public String deduct(Long productId, Integer count) {
        System.out.println("开始扣减库存...");
        
        // 查询库存
        Inventory inventory = inventoryRepository.findByProductId(productId);
        if (inventory == null) {
            throw new RuntimeException("库存不存在");
        }
        
        // 检查库存是否充足
        if (inventory.getCount() < count) {
            throw new RuntimeException("库存不足");
        }
        
        // 扣减库存
        inventory.setCount(inventory.getCount() - count);
        inventoryRepository.save(inventory);
        System.out.println("库存扣减成功:" + productId);
        
        return "库存扣减成功";
    }
}

4.4 账户服务实现

4.4.1 application.yml
yaml 复制代码
server:
  port: 8083

spring:
  application:
    name: account-service
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/account_db?useSSL=false&serverTimezone=UTC
    username: root
    password: root
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true

seata:
  enabled: true
  tx-service-group: my_test_tx_group
  # 其他配置同order-service
4.4.2 账户服务实现
java 复制代码
@Service
public class AccountService {
    
    @Autowired
    private AccountRepository accountRepository;
    
    /**
     * 扣减账户余额,分支事务
     */
    @Transactional(rollbackFor = Exception.class)
    public String deduct(Long userId, BigDecimal money) {
        System.out.println("开始扣减账户余额...");
        
        // 查询账户
        Account account = accountRepository.findByUserId(userId);
        if (account == null) {
            throw new RuntimeException("账户不存在");
        }
        
        // 检查余额是否充足
        if (account.getMoney().compareTo(money) < 0) {
            throw new RuntimeException("余额不足");
        }
        
        // 扣减余额
        account.setMoney(account.getMoney().subtract(money));
        accountRepository.save(account);
        System.out.println("账户余额扣减成功:" + userId);
        
        return "账户余额扣减成功";
    }
}

5. Seata Server部署

5.1 下载Seata Server

从Seata官网下载Seata Server 1.7.1:https://github.com/seata/seata/releases

5.2 配置Seata Server

5.2.1 registry.conf
conf 复制代码
registry {
  type = "nacos"
  nacos {
    application = "seata-server"
    serverAddr = "localhost:8848"
    group = "SEATA_GROUP"
    namespace = ""
    cluster = "default"
    username = "nacos"
    password = "nacos"
  }
}

config {
  type = "nacos"
  nacos {
    serverAddr = "localhost:8848"
    namespace = ""
    group = "SEATA_GROUP"
    username = "nacos"
    password = "nacos"
    dataId = "seataServer.properties"
  }
}
5.2.2 seataServer.properties

在Nacos中创建配置:

  • Data ID: seataServer.properties

  • Group: SEATA_GROUP

  • 配置内容:

    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://localhost:3306/seata?useSSL=false&serverTimezone=UTC
    store.db.user=root
    store.db.password=root
    store.db.minConn=5
    store.db.maxConn=30
    store.db.globalTable=global_table
    store.db.branchTable=branch_table
    store.db.lockTable=lock_table
    store.db.queryLimit=100
    store.db.lockTable=lock_table
    store.db.maxWait=5000

5.3 启动Seata Server

bash 复制代码
# 解压后进入bin目录
./seata-server.sh -h 127.0.0.1 -p 8091 -m db

6. 启动Nacos

bash 复制代码
# 解压后进入bin目录
./startup.sh -m standalone

7. 测试Seata AT模式

7.1 启动服务

  1. 启动Nacos
  2. 启动Seata Server
  3. 启动order-service
  4. 启动inventory-service
  5. 启动account-service

7.2 测试正常流程

7.2.1 发送请求
bash 复制代码
curl -X POST -H "Content-Type: application/json" -d '{"userId":1,"productId":1,"count":10,"money":100.00}' http://localhost:8081/order/create
7.2.2 预期结果
  • 订单服务:创建订单成功
  • 库存服务:库存从100减到90
  • 账户服务:账户余额从1000减到0
  • 全局事务提交成功

7.3 测试回滚流程

7.3.1 修改订单服务代码

OrderService.createOrder方法中添加异常:

java 复制代码
// 模拟异常,测试回滚
if (true) throw new RuntimeException("模拟异常,测试回滚");
7.3.2 发送请求

同7.2.1

7.3.3 预期结果
  • 订单服务:订单创建后回滚
  • 库存服务:库存从100减到90后回滚到100
  • 账户服务:账户余额从1000减到0后回滚到1000
  • 全局事务回滚成功

8. Seata AT模式核心机制

8.1 SQL执行与Undo Log生成

8.1.1 执行SQL
sql 复制代码
UPDATE inventory SET count = count - 10 WHERE product_id = 1;
8.1.2 生成Undo Log

Seata会自动拦截SQL,生成Undo Log并写入数据库:

sql 复制代码
INSERT INTO undo_log (
    branch_id, xid, context, rollback_info, log_status, log_created, log_modified
) VALUES (
    123456, 'xid-123', 'serializable', '{"beforeImage":{"count":100},"afterImage":{"count":90}}', 1, NOW(), NOW()
);

8.2 事务回滚机制

8.2.1 回滚流程
  1. TC通知RM执行回滚
  2. RM读取Undo Log
  3. RM执行回滚SQL:UPDATE inventory SET count = 100 WHERE product_id = 1 AND count = 90;
  4. RM删除Undo Log
8.2.2 回滚条件
  • 乐观锁机制:回滚SQL包含当前值条件,确保数据未被其他事务修改
  • 幂等性:多次回滚操作结果一致

9. 监控与管理

9.1 Seata控制台

Seata提供了Web控制台,用于监控全局事务状态:

9.2 核心监控指标

  • 全局事务数量:当前活跃的全局事务数量
  • 分支事务数量:当前活跃的分支事务数量
  • 事务成功率:全局事务成功率
  • 事务平均耗时:全局事务平均执行时间
  • 回滚事务数量:回滚的全局事务数量

10. 最佳实践

10.1 配置优化

  • 数据库连接池:增大连接池大小,支持更多并发事务
  • Undo Log清理:定期清理过期的Undo Log,避免磁盘空间占用过大
  • 超时设置:合理设置全局事务超时时间,避免事务长时间占用资源

10.2 开发注意事项

  • @GlobalTransactional:仅在事务发起方添加,分支事务无需添加
  • 本地事务 :分支事务仍需添加@Transactional注解
  • 幂等性:业务逻辑需考虑幂等性,避免重复执行
  • 异常处理:合理处理异常,确保事务能正确回滚
  • 避免长事务:全局事务应尽可能短,减少资源占用

10.3 部署建议

  • Seata Server高可用:部署多个Seata Server实例,避免单点故障
  • 数据库分离:Seata数据库与业务数据库分离,提高性能
  • 网络优化:确保Seata Server与业务服务网络通畅,减少网络延迟

11. 常见问题与解决方案

11.1 问题:全局事务未生效

解决方案

  • 检查@GlobalTransactional注解是否添加
  • 检查XID是否正确传递(通过Feign拦截器)
  • 检查Seata配置是否正确

11.2 问题:事务回滚失败

解决方案

  • 检查Undo Log表是否正确创建
  • 检查数据库权限是否足够
  • 检查乐观锁条件是否满足(数据是否被其他事务修改)

11.3 问题:性能问题

解决方案

  • 优化SQL,减少执行时间
  • 增大Seata Server的线程池大小
  • 考虑拆分大事务为多个小事务

12. 总结

Seata AT模式是一种非侵入式的分布式事务解决方案,通过自动生成Undo Log和两阶段提交,实现了分布式事务的自动管理。其核心优势包括:

  • 非侵入式:无需修改业务代码,只需添加@GlobalTransactional注解
  • 自动回滚:自动生成Undo Log,支持事务回滚
  • 高性能:异步提交,减少锁竞争
  • 易于集成:与Spring Cloud、Dubbo等框架无缝集成
  • 可靠性高:支持事务回滚,确保数据一致性

通过本实例,您可以深入理解Seata AT模式的工作原理和实现细节,并在实际项目中应用Seata AT模式解决分布式事务问题。

相关推荐
人道领域2 小时前
【零基础学java】(Map集合)
java·开发语言
杀死那个蝈坦2 小时前
JUC并发编程day1
java·开发语言
飞Link2 小时前
【Java】Linux(CentOS7)下安装JDK8(Java)教程
java·linux·运维·服务器
秋4272 小时前
基于tomcat的动静分离
java·tomcat
巨人张2 小时前
C++零基础游戏----“大鱼吃小鱼”
java·c++·游戏
伯明翰java2 小时前
Java接口
java·开发语言
云和数据.ChenGuang2 小时前
Java装箱与拆箱(面试核心解析)
java·开发语言·面试
SimonKing2 小时前
MyBatis的隐形炸弹:selectByExampleWithBLOBs使用不当,让性能下降80%
java·后端·程序员
海南java第二人2 小时前
打破Java双亲委派模型的三大核心场景与技术实现
java·spring