下载工作台
Spring Cloud 微服务

分布式事务 Seata 入门

试读上半部分 · 解锁后可读全文

第 7 章 · Seata AT 模式分布式事务入门

本章目标:理解微服务场景下的分布式事务问题;掌握 Seata 架构(TC、TM、RM)与 AT 模式原理(一阶段提交、undo log);在 svc-spring-demo 搭建教学级「下单扣库存」跨服务事务演示;对比 AT 与 TCCSaga本地消息表;知晓性能开销与使用边界,避免滥用全局事务。

学时建议:6~7 小时(含 2.5 小时 Seata Server 与双服务联调)

前置spring-cloud-web ch01~ch06spring-boot-web ch05 JPA 事务;MySQL InnoDB 基础。


7.1 问题:跨服务数据一致性

ch04 order-svc 写订单库,ch06 inventory-svc 异步扣库存——最终一致,但存在短暂「订单已建、库存未扣」窗口。

部分业务要求强一致(教学简化场景):

用户下单成功 ⇔ 订单已创建 且 库存已扣减

单体 shop-spring-demo 用一个 @Transactional 搞定;微服务跨库跨进程需要 分布式事务

方案一致性复杂度适用
本地事务单库强一致单体
Seata AT强一致(默认读未提交隔离讨论)短事务、关系型 DB
TCC需预留、确认、取消
Saga最终一致长流程
本地消息表最终一致高吞吐写路径

本章聚焦 Seata AT 入门;生产需评估是否真的需要全局事务。

虚构环境:Seata TC 使用 seata.example.com:8091 或本地 Docker;严禁连接公司生产 TC 或真实业务库。

7.2 Seata 组件架构

┌─────────────┐     注册      ┌──────────────────┐
│  Seata TC   │◄─────────────│  Nacos           │
│  (Server)   │               │  nacos.example.com│
└──────┬──────┘               └──────────────────┘
       │ 协调全局事务
       │
   ┌───┴───┐         ┌──────────────┐
   │ order │         │ inventory    │
   │ -svc  │         │ -svc         │
   │ (TM)  │────────►│ (RM)         │
   └───┬───┘  Feign  └──────┬───────┘
       │                    │
   order_db            inventory_db
角色全称职责
TCTransaction Coordinator维护全局/分支事务状态,驱动提交或回滚
TMTransaction Manager开启、提交或回滚全局事务(通常订单服务)
RMResource Manager管理分支事务,上报 TC,执行本地提交/回滚

客户端通过 Nacos 发现 TC 地址(与 ch02 一致)。


7.3 AT 模式原理(两阶段简化理解)

一阶段(业务 SQL 执行时):

  1. 解析 SQL,查询前镜像(before image)
  2. 执行业务 SQL
  3. 查询后镜像(after image)
  4. 本地事务提交业务数据 + 插入 undo_log(同一本地事务)
  5. 向 TC 注册分支,申请全局锁(防脏写)

二阶段提交

  • 全局提交:异步删除 undo_log
  • 全局回滚:根据 undo_log 生成反向 SQL 恢复数据
         一阶段                    二阶段
order-svc: INSERT order  ──►  TC 决定 COMMIT ──► 删 undo_log
inventory: UPDATE stock  ──►              └──► 或 ROLLBACK ──► 用 undo 恢复
特点说明
对业务侵入低,主要加 @GlobalTransactional
数据源需代理数据源(Seata 自动)
隔离级别默认 READ UNCOMMITTED 读,靠全局锁防写冲突
限制不支持部分 NoSQL;跨服务调用链不宜过长

7.4 启动 Seata Server(Docker 教学)

docker-compose.dev.yml

  seata-server:
    image: seataio/seata-server:2.0.0   # 官方镜像为 seataio/seata-server
    ports:
      - "8091:8091"
    environment:
      SEATA_PORT: 8091
      STORE_MODE: file
    depends_on:
      - nacos

Seata 向 Nacos 注册,配置 registry.conf / application.yml 指向 nacos.example.com:8848(本地 127.0.0.1)。

store.mode 教学可用 file;生产用 db 存事务会话。


7.5 业务库准备:undo_log 表

每个参与 AT 的 MySQL 库执行:

CREATE TABLE IF NOT EXISTS undo_log (
    id            BIGINT       NOT NULL AUTO_INCREMENT,
    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(6)  NOT NULL,
    log_modified  DATETIME(6)  NOT NULL,
    PRIMARY KEY (id),
    UNIQUE KEY ux_undo_log (xid, branch_id)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

order_dbinventory_db 各一份


7.6 order-svc 集成 Seata(TM)

7.6.1 依赖

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>

7.6.2 配置

seata:
  enabled: true
  application-id: order-svc
  tx-service-group: svc-spring-demo-tx-group
  registry:
    type: nacos
    nacos:
      server-addr: ${NACOS_ADDR:127.0.0.1:8848}
      namespace: ${NACOS_NAMESPACE:dev}
      group: SEATA_GROUP
  config:
    type: nacos
    nacos:
      server-addr: ${NACOS_ADDR:127.0.0.1:8848}
      namespace: ${NACOS_NAMESPACE:dev}
      group: SEATA_GROUP

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/order_db?useSSL=false&serverTimezone=UTC
    username: ${DB_USER:demo}
    password: ${DB_PASSWORD:demo}

在 Nacos 配置 Seata 服务端识别的 service.vgroupMapping.svc-spring-demo-tx-group=default 等(按 Seata 2.x 文档模板,教学 compose 可预置)。

7.6.3 全局事务入口

@Service
@RequiredArgsConstructor
@Slf4j
public class OrderSeataService {

    private final OrderRepository orderRepository;
    private final InventoryClient inventoryClient;
    private final ProductClient productClient;

    @GlobalTransactional(name = "create-order-tx", rollbackFor = Exception.class)
    public OrderDto createOrderWithStock(CreateOrderCommand cmd) {
        Result<ProductDto> productRes = productClient.getById(cmd.getProductId());
        ProductDto product = productRes.getData();
        if (product == null || !product.isOnSale()) {
            throw new BusinessException(ErrorCode.PRODUCT_UNAVAILABLE);
        }

        Order order = new Order();
        order.setUserId(cmd.getUserId());
        order.setProductId(cmd.getProductId());
        order.setQuantity(cmd.getQuantity());
        order.setAmount(product.getPrice().multiply(BigDecimal.valueOf(cmd.getQuantity())));
        order.setStatus(OrderStatus.PAID);
        orderRepository.save(order);

        Result<Void> deductRes = inventoryClient.deduct(
            cmd.getProductId(), cmd.getQuantity(), order.getId());
        if (!deductRes.isSuccess()) {
            throw new BusinessException(ErrorCode.STOCK_DEDUCT_FAILED);
        }

        log.info("Global tx success orderId={}", order.getId());
        return OrderMapper.toDto(order);
    }
}

@GlobalTransactional 标记 TM;方法内 Feign 调用 inventory 成为同一 XID 下的分支。


以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。