跳转到内容
123xiao | 无名键客

《微服务架构中分布式事务的实战方案:基于 Seata 的一致性设计与落地经验》

字数: 0 阅读时长: 1 分钟

微服务架构中分布式事务的实战方案:基于 Seata 的一致性设计与落地经验

微服务拆开之后,单体时代“一个本地事务包打天下”的日子就结束了。下单、扣库存、冻结余额、生成物流单,往往分散在不同服务、不同库里。业务链路一长,数据一致性问题就会很快冒出来:订单成功了,库存没扣;库存扣了,支付回滚了;消息发了,数据库却没提交。

如果你也在做这类系统,Seata 基本绕不过去。它不是银弹,但在“关系型数据库 + Java 微服务 + 对一致性有明确要求”的场景里,确实是很实用的一套方案。本文我会从架构设计、原理、可运行代码、排障经验、性能与安全边界几个角度,把 Seata 的落地过程讲清楚,尽量像带你走一遍项目实践,而不是只讲概念。


背景与问题

先看一个典型链路:用户提交订单时,需要同时完成几件事:

  1. 订单服务创建订单
  2. 库存服务扣减库存
  3. 账户服务扣减余额
  4. 成功后订单状态改为“已确认”

这在单体里通常是一个数据库事务。但拆成微服务后,事务边界天然被切断了。

为什么本地事务不够用了

每个服务内部仍然可以用本地事务保证一致性,但服务之间的调用就失去了统一提交/回滚能力。最常见的问题有三类:

  • 部分成功:订单写入成功,库存服务超时
  • 重试导致重复执行:调用方超时后重试,扣库存扣了两次
  • 异步链路难回溯:消息已经投递,业务状态却不完整

很多团队一开始会用“失败补偿 + 人工修单”顶着,但业务量上来后,这种方式成本极高,而且非常依赖人。

分布式事务方案并不只有一种

在做方案前,先明确一点:不是所有业务都需要强一致分布式事务

常见方案大概是这几类:

方案一致性复杂度性能适用场景
2PC/XA强一致较差少量核心金融场景
Seata AT最终接近强一致较好RDBMS、侵入低
Seata TCC强业务可控核心链路、可预留资源
Saga最终一致中高长事务、流程型业务
本地消息表/Outbox最终一致事件驱动、异步链路

如果你的链路是数据库更新为主、服务间同步调用较多、希望尽量少改业务代码,Seata 的 AT 模式通常是第一选择。
如果你的业务是“冻结/确认/取消”模型明显,比如支付、券核销、账户额度,那 TCC 模式更合适。


方案对比与取舍分析

这部分很关键。很多项目不是技术做不到,而是方案选型时没把边界想清楚。

Seata AT 适合什么

AT 模式最大的优势是:

  • 基于 JDBC 数据源代理
  • 大多数业务代码改动少
  • 对已有 Spring Boot / MyBatis 项目接入友好
  • 自动生成 undo log,支持回滚

但它也有明显前提:

  • 主要依赖关系型数据库
  • SQL 不能太“野”
  • 全局锁会带来一定性能影响
  • 跨库跨服务强一致能力依赖 TC、RM、TM 协同

Seata TCC 适合什么

TCC 的优点在于业务控制力强:

  • 明确的 Try / Confirm / Cancel 三段
  • 不依赖数据库 undo log
  • 对高并发、可预留资源的场景更稳定

缺点也很现实:

  • 业务侵入大
  • 开发成本高
  • 空回滚、悬挂、幂等等问题要自己兜住

一个实用判断标准

我在项目里通常这样判断:

  • 先问业务是否允许最终一致
    • 可以:优先考虑消息表/Outbox、Saga
  • 如果必须同步成功失败
    • 再看是否主要操作数据库
  • 如果数据库更新为主,且希望低侵入
    • 优先 Seata AT
  • 如果资源预留模型天然存在
    • 优先 TCC

一句话总结:AT 更像“工程上好落地”,TCC 更像“业务上更可控”。


核心原理

下面以 Seata AT 模式为主讲,因为它最常用于微服务项目的第一阶段落地。

Seata 里的三个角色

  • TC(Transaction Coordinator):事务协调器,维护全局事务和分支事务状态
  • TM(Transaction Manager):事务管理器,负责开启/提交/回滚全局事务
  • RM(Resource Manager):资源管理器,管理分支事务和本地资源

整体协作关系如下:

flowchart LR
    A[客户端请求] --> B[订单服务 TM]
    B --> C[TC 协调器]
    B --> D[订单库 RM]
    B --> E[库存服务 RM]
    B --> F[账户服务 RM]
    C --> D
    C --> E
    C --> F

AT 模式做了什么

AT 模式的核心思路是:

  1. 拦截业务 SQL
  2. 在提交前生成 before imageafter image
  3. 把回滚信息记录到 undo_log
  4. 分支事务先按本地事务提交
  5. 全局事务成功则整体提交;失败则利用 undo_log 反向补偿

也就是说,它并不是像 XA 那样长时间锁住所有资源,而是借助日志和全局锁实现“对业务较透明”的回滚能力。

一次下单的时序

sequenceDiagram
    participant Client as 用户
    participant Order as 订单服务(TM)
    participant TC as Seata TC
    participant Stock as 库存服务(RM)
    participant Account as 账户服务(RM)

    Client->>Order: 提交下单请求
    Order->>TC: 开启全局事务
    TC-->>Order: 返回 XID
    Order->>Order: 创建订单(本地事务)
    Order->>Stock: 扣减库存(XID 透传)
    Stock->>Stock: 写 undo_log + 扣库存
    Order->>Account: 扣减余额(XID 透传)
    Account->>Account: 写 undo_log + 扣余额
    Order->>TC: 提交全局事务
    TC-->>Order: 全局提交成功
    Order-->>Client: 下单成功

如果账户扣减失败,TC 会通知已成功的分支回滚。

全局锁的意义

AT 模式会维护一类逻辑锁,避免多个全局事务同时修改同一批数据导致回滚混乱。
这也是为什么在高并发热点数据场景里,AT 模式会出现锁冲突、重试等待甚至吞吐下降。

状态变化简图

stateDiagram-v2
    [*] --> Begin: 开启全局事务
    Begin --> BranchRegistered: 分支注册成功
    BranchRegistered --> GlobalCommit: 所有分支成功
    BranchRegistered --> GlobalRollback: 任一分支失败
    GlobalCommit --> [*]
    GlobalRollback --> RollbackRetrying: 回滚重试
    RollbackRetrying --> [*]

实战代码(可运行)

下面给一个可运行的简化示例:订单服务调用库存服务和账户服务,使用 Spring Boot + OpenFeign + Seata AT。

说明:代码偏最小可跑通版本,便于理解核心接入点。生产环境还要补充鉴权、限流、日志、监控等。


环境准备

  • JDK 8+
  • Spring Boot 2.x
  • Seata 1.4+(示例写法兼容常见版本)
  • MySQL 5.7 / 8.x
  • Nacos 或 file.conf 注册中心配置方式均可

核心表结构

订单表

CREATE TABLE `t_order` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `user_id` bigint NOT NULL,
  `product_id` bigint NOT NULL,
  `count` int NOT NULL,
  `money` decimal(10,2) NOT NULL,
  `status` int NOT NULL DEFAULT 0,
  PRIMARY KEY (`id`)
);

库存表

CREATE TABLE `t_stock` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `product_id` bigint NOT NULL,
  `total` int NOT NULL,
  `used` int NOT NULL,
  `residue` int NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_product_id` (`product_id`)
);

账户表

CREATE TABLE `t_account` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `user_id` bigint NOT NULL,
  `total` decimal(10,2) NOT NULL,
  `used` decimal(10,2) NOT NULL,
  `residue` decimal(10,2) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_user_id` (`user_id`)
);

Seata 必需的 undo_log 表

每个参与 AT 事务的业务库都要建:

CREATE TABLE `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 NOT NULL,
  `log_modified` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
);

订单服务示例

Maven 依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>

    <dependency>
        <groupId>io.seata</groupId>
        <artifactId>seata-spring-boot-starter</artifactId>
        <version>1.5.2</version>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>

    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.2</version>
    </dependency>
</dependencies>

application.yml

server:
  port: 8081

spring:
  application:
    name: order-service
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/seata_order?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver

seata:
  enabled: true
  application-id: order-service
  tx-service-group: my_test_tx_group
  service:
    vgroup-mapping:
      my_test_tx_group: default
    grouplist:
      default: 127.0.0.1:8091
  enable-auto-data-source-proxy: true

启动类

package com.example.order;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients
@MapperScan("com.example.order.mapper")
public class OrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
}

实体类

package com.example.order.entity;

import java.math.BigDecimal;

public class Order {
    private Long id;
    private Long userId;
    private Long productId;
    private Integer count;
    private BigDecimal money;
    private Integer status;

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }

    public Long getUserId() { return userId; }
    public void setUserId(Long userId) { this.userId = userId; }

    public Long getProductId() { return productId; }
    public void setProductId(Long productId) { this.productId = productId; }

    public Integer getCount() { return count; }
    public void setCount(Integer count) { this.count = count; }

    public BigDecimal getMoney() { return money; }
    public void setMoney(BigDecimal money) { this.money = money; }

    public Integer getStatus() { return status; }
    public void setStatus(Integer status) { this.status = status; }
}

Mapper

package com.example.order.mapper;

import com.example.order.entity.Order;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Update;
import org.apache.ibatis.annotations.Options;

@Mapper
public interface OrderMapper {

    @Insert("INSERT INTO t_order(user_id, product_id, count, money, status) VALUES(#{userId}, #{productId}, #{count}, #{money}, #{status})")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    int insert(Order order);

    @Update("UPDATE t_order SET status = #{status} WHERE id = #{id}")
    int updateStatus(Long id, Integer status);
}

Feign 接口

package com.example.order.client;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.math.BigDecimal;

@FeignClient(name = "stock-service", url = "http://127.0.0.1:8082")
public interface StockClient {

    @PostMapping("/stock/decrease")
    String decrease(@RequestParam("productId") Long productId,
                    @RequestParam("count") Integer count);
}
package com.example.order.client;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.math.BigDecimal;

@FeignClient(name = "account-service", url = "http://127.0.0.1:8083")
public interface AccountClient {

    @PostMapping("/account/decrease")
    String decrease(@RequestParam("userId") Long userId,
                    @RequestParam("money") BigDecimal money);
}

Service

package com.example.order.service;

import com.example.order.client.AccountClient;
import com.example.order.client.StockClient;
import com.example.order.entity.Order;
import com.example.order.mapper.OrderMapper;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;

@Service
public class OrderService {

    private final OrderMapper orderMapper;
    private final StockClient stockClient;
    private final AccountClient accountClient;

    public OrderService(OrderMapper orderMapper, StockClient stockClient, AccountClient accountClient) {
        this.orderMapper = orderMapper;
        this.stockClient = stockClient;
        this.accountClient = accountClient;
    }

    @GlobalTransactional(name = "create-order-tx", rollbackFor = Exception.class)
    public Long createOrder(Long userId, Long productId, Integer count, BigDecimal money) {
        Order order = new Order();
        order.setUserId(userId);
        order.setProductId(productId);
        order.setCount(count);
        order.setMoney(money);
        order.setStatus(0);
        orderMapper.insert(order);

        stockClient.decrease(productId, count);
        accountClient.decrease(userId, money);

        orderMapper.updateStatus(order.getId(), 1);
        return order.getId();
    }
}

Controller

package com.example.order.controller;

import com.example.order.service.OrderService;
import org.springframework.web.bind.annotation.*;

import java.math.BigDecimal;

@RestController
@RequestMapping("/order")
public class OrderController {

    private final OrderService orderService;

    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    @PostMapping("/create")
    public String create(@RequestParam Long userId,
                         @RequestParam Long productId,
                         @RequestParam Integer count,
                         @RequestParam BigDecimal money) {
        Long orderId = orderService.createOrder(userId, productId, count, money);
        return "success, orderId=" + orderId;
    }
}

库存服务示例

application.yml

server:
  port: 8082

spring:
  application:
    name: stock-service
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/seata_stock?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver

seata:
  enabled: true
  application-id: stock-service
  tx-service-group: my_test_tx_group
  service:
    vgroup-mapping:
      my_test_tx_group: default
    grouplist:
      default: 127.0.0.1:8091
  enable-auto-data-source-proxy: true

Mapper

package com.example.stock.mapper;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Update;

@Mapper
public interface StockMapper {

    @Update("UPDATE t_stock SET used = used + #{count}, residue = residue - #{count} WHERE product_id = #{productId} AND residue >= #{count}")
    int decrease(Long productId, Integer count);
}

Service + Controller

package com.example.stock.service;

import com.example.stock.mapper.StockMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class StockService {

    private final StockMapper stockMapper;

    public StockService(StockMapper stockMapper) {
        this.stockMapper = stockMapper;
    }

    @Transactional(rollbackFor = Exception.class)
    public void decrease(Long productId, Integer count) {
        int updated = stockMapper.decrease(productId, count);
        if (updated == 0) {
            throw new RuntimeException("库存不足");
        }
    }
}
package com.example.stock.controller;

import com.example.stock.service.StockService;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/stock")
public class StockController {

    private final StockService stockService;

    public StockController(StockService stockService) {
        this.stockService = stockService;
    }

    @PostMapping("/decrease")
    public String decrease(@RequestParam Long productId, @RequestParam Integer count) {
        stockService.decrease(productId, count);
        return "ok";
    }
}

账户服务示例

application.yml

server:
  port: 8083

spring:
  application:
    name: account-service
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/seata_account?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver

seata:
  enabled: true
  application-id: account-service
  tx-service-group: my_test_tx_group
  service:
    vgroup-mapping:
      my_test_tx_group: default
    grouplist:
      default: 127.0.0.1:8091
  enable-auto-data-source-proxy: true

Mapper

package com.example.account.mapper;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Update;

import java.math.BigDecimal;

@Mapper
public interface AccountMapper {

    @Update("UPDATE t_account SET used = used + #{money}, residue = residue - #{money} WHERE user_id = #{userId} AND residue >= #{money}")
    int decrease(Long userId, BigDecimal money);
}

Service + 故障注入

package com.example.account.service;

import com.example.account.mapper.AccountMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;

@Service
public class AccountService {

    private final AccountMapper accountMapper;

    public AccountService(AccountMapper accountMapper) {
        this.accountMapper = accountMapper;
    }

    @Transactional(rollbackFor = Exception.class)
    public void decrease(Long userId, BigDecimal money) {
        int updated = accountMapper.decrease(userId, money);
        if (updated == 0) {
            throw new RuntimeException("余额不足");
        }

        // 用于验证全局回滚
        if (money.compareTo(new BigDecimal("1000")) > 0) {
            throw new RuntimeException("模拟账户服务异常");
        }
    }
}
package com.example.account.controller;

import com.example.account.service.AccountService;
import org.springframework.web.bind.annotation.*;

import java.math.BigDecimal;

@RestController
@RequestMapping("/account")
public class AccountController {

    private final AccountService accountService;

    public AccountController(AccountService accountService) {
        this.accountService = accountService;
    }

    @PostMapping("/decrease")
    public String decrease(@RequestParam Long userId, @RequestParam BigDecimal money) {
        accountService.decrease(userId, money);
        return "ok";
    }
}

如何验证分布式事务生效

初始化数据

INSERT INTO t_stock(product_id, total, used, residue) VALUES(1, 100, 0, 100);
INSERT INTO t_account(user_id, total, used, residue) VALUES(1, 2000.00, 0.00, 2000.00);

正常请求

curl -X POST "http://127.0.0.1:8081/order/create?userId=1&productId=1&count=2&money=100"

预期结果:

  • t_order 新增一条,状态为 1
  • t_stock.residue 减少 2
  • t_account.residue 减少 100

异常请求,触发全局回滚

curl -X POST "http://127.0.0.1:8081/order/create?userId=1&productId=1&count=2&money=1200"

因为账户服务里故意抛了异常,预期结果:

  • 订单记录回滚,不存在或未提交
  • 库存扣减回滚
  • 账户扣减回滚

如果数据库状态恢复到调用前,说明 Seata AT 已经工作起来了。


常见坑与排查

这一部分是我觉得最有实战价值的。因为 Seata 接入最痛的,往往不是“写代码”,而是“明明配了就是不生效”。

1. 全局事务没生效,只有本地事务提交了

现象:

  • 下游服务异常了
  • 上游服务抛错
  • 但订单表已经提交,库存也改了

优先检查:

  1. 是否真的加了 @GlobalTransactional
  2. 注解是否加在对外调用入口的方法上
  3. 是否存在同类自调用导致 AOP 失效
  4. 是否启用了 seata-spring-boot-starter
  5. 数据源是否被 Seata 代理

排查建议:

看日志里有没有 XID,例如:

Begin new global transaction
xid = 192.168.0.1:8091:1234567890

如果调用链路中没有 XID,基本说明全局事务根本没开启。


2. Feign 调用了,但 XID 没传过去

现象:

  • 订单服务有全局事务日志
  • 库存/账户服务看起来像普通本地事务
  • 回滚只回滚了部分服务

这通常是 XID 透传失败
新版本 Seata 对 Spring Cloud 集成已经方便很多,但如果你用了自定义 Feign、Gateway、异步线程池,还是可能丢上下文。

建议:

  • 先用官方兼容方式接入
  • 检查是否自定义了请求拦截器覆盖了 Seata 透传逻辑
  • 对线程池、异步任务显式传递事务上下文

3. 忘记建 undo_log

这是最常见也最“低级”的坑,但真不少见。

现象:

  • 分支注册异常
  • 回滚时报错
  • 某个服务始终参与不了全局事务

排查方式:

确认每个参与 AT 事务的库里都建了 undo_log,并且结构版本匹配。


4. SQL 太复杂,AT 回滚异常

Seata AT 并不是能透明支持所有 SQL。以下场景要特别小心:

  • 多表复杂更新
  • 非标准 SQL
  • 大批量更新
  • 带函数计算的复杂字段修改
  • select for update 与业务锁混用

建议:

  • 核心事务 SQL 尽量简单、可预测
  • 一条更新尽量聚焦单表主键/唯一键
  • 热点行少做大事务串联

我踩过一个坑:某次用了较复杂的批量更新,业务成功率看着还行,但回滚镜像异常,最后只能拆成多条明确更新 SQL。


5. 全局锁冲突,吞吐明显下降

现象:

  • 高峰期接口 RT 飙升
  • TC 日志出现 lock conflict
  • 重试变多

这不是 bug,而是设计代价。AT 模式为了保证一致性,需要对修改数据加全局锁,热点数据就容易冲突。

排查思路:

  • 看是不是同一商品、同一账户、同一库存行被高频更新
  • 看事务持续时间是否过长
  • 看是否在全局事务中做了 RPC 之外的慢操作

优化方向:

  • 缩小事务范围
  • 把非必要逻辑移出全局事务
  • 业务上拆热点
  • 对库存做分段、分桶

6. 回滚一直重试,数据状态不干净

现象:

  • TC 中分支状态迟迟不结束
  • 回滚持续重试
  • 数据被人工改过后回不去

AT 回滚依赖 before image / after image 校验,如果业务数据被外部修改,可能导致脏写防护触发,回滚失败。

建议:

  • 不要绕过服务直接手工改库
  • 回滚失败时先看 undo_log 和镜像校验日志
  • 人工修数前先冻结相关链路

常见日志定位路径

我通常按这个顺序看:

flowchart TD
    A[请求失败或数据不一致] --> B{订单服务有无 XID?}
    B -- 否 --> C[检查 @GlobalTransactional 与代理生效]
    B -- 是 --> D{下游服务日志有无 XID?}
    D -- 否 --> E[检查 Feign/网关/异步透传]
    D -- 是 --> F{是否存在 undo_log?}
    F -- 否 --> G[补齐表结构和数据源代理]
    F -- 是 --> H[检查锁冲突/SQL兼容性/镜像回滚]

这个排查顺序很省时间,因为它先排“有没有进入 Seata 世界”,再排“进入后哪里断了”。


安全/性能最佳实践

分布式事务一旦进生产,讨论就不能停留在“能不能回滚”,还要考虑性能和稳定性。

安全最佳实践

1. 不要把 Seata 当成业务幂等的替代品

Seata 解决的是分布式事务协调,不是业务接口天然幂等。

例如:

  • 下单接口要有业务单号防重
  • 扣库存接口要支持幂等检查
  • 账户扣减要避免重复消费

如果只靠事务,遇到超时重试、网关重放、消息重复投递,还是会出问题。

2. 敏感操作要保留审计日志

像账户、额度、优惠券这类敏感资源,建议保留:

  • 请求流水号
  • 全局事务 XID
  • 业务主键
  • 调用结果
  • 回滚原因

这样真出问题时,不至于只能看数据库最终状态“猜”发生了什么。

3. 避免把外部不可控调用放进全局事务

比如:

  • 第三方支付
  • 外部物流接口
  • 短信、邮件、推送平台

这些接口不可控、耗时长、失败原因复杂,放进全局事务会显著放大风险。更合理的做法是:

  • 核心资源先落本地一致状态
  • 外部调用通过消息异步驱动
  • 对失败结果做补偿和人工介入

性能最佳实践

1. 缩短全局事务时间

这是最重要的一条。
全局事务不是越大越安全,而是越短越稳

尽量只把这些放进去:

  • 订单创建
  • 库存预扣/扣减
  • 账户冻结/扣减

不要把这些放进去:

  • 参数组装
  • 远程查询列表
  • 调用推荐、营销、画像服务
  • 发通知、写大日志、查报表

2. 热点资源拆分

如果某个商品、账户、券池特别热,AT 全局锁会让冲突很明显。可用手段包括:

  • 库存分桶
  • 账户子账本
  • 按区域或仓库拆库存
  • 削峰到队列,改同步为预占 + 异步确认

3. 监控这些关键指标

生产上至少要监控:

  • 全局事务数量
  • 提交成功率 / 回滚成功率
  • 分支事务耗时
  • 锁冲突次数
  • 回滚重试次数
  • TC CPU / 内存 / 连接数
  • undo_log 增长情况

4. 做容量估算

一个粗略估算思路:

  • 峰值 TPS:500
  • 平均每个全局事务 3 个分支
  • 平均事务时长:150ms

则瞬时活跃分支事务量大约是:

500 * 3 * 0.15 = 225

如果再考虑重试、尖峰放大、热点冲突,TC 和数据库都要留足余量。
经验上,先压测再上线,比事后看日志救火靠谱得多。


什么时候不建议用 Seata AT

这部分很重要,避免“拿着锤子看什么都像钉子”。

以下场景,我一般不建议直接上 AT:

  1. 超高并发热点写场景

    • 比如秒杀扣单行库存
    • 全局锁冲突会很痛
  2. 事务跨度很长

    • 包含人工审核、外部回调、分钟级状态流转
    • 更适合 Saga 或状态机
  3. 数据库类型复杂

    • 非标准 RDBMS、多种异构存储混用
    • AT 适配成本会升高
  4. SQL 非常复杂

    • 大批量、复杂联表、强依赖自定义语句
    • 回滚镜像和兼容性风险大

这时候更适合考虑:

  • TCC
  • Saga
  • Outbox + 消息最终一致
  • 业务状态机 + 补偿

落地建议:一个真实可执行的分层思路

如果团队第一次接入分布式事务,我建议按下面节奏来,不要一上来就全链路铺开。

第 1 步:先圈核心链路

只挑最关键的一条,比如:

  • 下单
  • 扣库存
  • 扣余额

先把这条链路跑通,并验证:

  • 正常提交
  • 异常回滚
  • 超时重试
  • 下游异常
  • 回滚恢复

第 2 步:给接口补幂等

至少补这些:

  • 请求唯一号
  • 订单号防重
  • 扣减接口去重表或业务状态校验

第 3 步:再做监控和压测

不要等到线上才关注锁冲突和回滚重试。
压测时重点看:

  • 事务平均耗时
  • 高峰时 lock conflict
  • TC 负载
  • DB 慢 SQL

第 4 步:识别不适合 Seata 的链路

把以下链路逐步移出去:

  • 外部三方调用
  • 耗时不可控逻辑
  • 通知、埋点、异步派生数据

最后会形成一个比较健康的架构:
核心资源变更用 Seata,外围副作用用消息和补偿。


总结

Seata 在微服务分布式事务场景里,最大的价值不是“让你重新回到单体时代”,而是提供了一种可工程化落地的一致性能力

如果你的系统满足这些条件:

  • 主要是 Java 微服务
  • 数据落在关系型数据库
  • 核心链路要求同步一致
  • 希望尽量少改现有业务代码

那么 Seata AT 是很值得优先尝试的方案
但要记住它的边界:

  • 它不替代业务幂等
  • 它不适合长事务和超热点写
  • 它也不该包住所有外部调用

我的实战建议可以压缩成三句:

  1. 先用 AT 跑通最核心链路,不要贪大求全
  2. 把事务做短,把 SQL 做简单,把幂等补完整
  3. 一旦出现热点冲突或长事务,果断考虑 TCC/Saga/消息补偿

真正稳定的分布式一致性设计,从来不是“选了 Seata 就结束”,而是知道什么时候该用它,什么时候该收手


分享到:

上一篇
《Spring Boot 3 中基于 JWT 与 Spring Security 6 的前后端分离权限认证实战》
下一篇
《Java中基于CompletableFuture构建高并发异步任务编排的实战指南》