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

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

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

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

在单体应用里,一个数据库事务就能兜住“下单、扣库存、扣余额、生成物流单”这一整串操作;可一旦拆成微服务,这些动作分散到不同服务、不同数据库,原来那句简单的 @Transactional 就不够用了。

很多团队第一次碰到分布式事务,往往会在两种极端之间摇摆:

  • 要么强行追求“像单库事务一样绝对一致”,结果架构复杂、性能很差;
  • 要么干脆只讲最终一致性,但缺少补偿、对账、幂等等配套设计,线上数据越跑越歪。

这篇文章我想从工程落地的角度,带你把 Seata 这件事讲透:什么时候用、怎么用、代码怎么写、坑怎么排、边界怎么守。如果你已经有 Spring Cloud / Spring Boot 微服务基础,这篇内容应该能直接用于项目设计。


背景与问题

先看一个很典型的业务链路:电商下单。

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

如果每个服务都有自己的数据库,那么一个用户下单动作,实际上变成了多个本地事务的组合。问题在于:

  • 订单写成功了,但库存扣减失败;
  • 库存扣了,账户没扣成功;
  • 某个服务成功执行了,但调用方网络超时,不知道到底要不要重试;
  • 服务重试后,重复扣库存或重复扣钱。

本质问题是什么?

分布式事务本质上是在解决两个问题:

  • 跨多个资源的一致性
  • 异常场景下的可恢复性

而 CAP、网络分区、服务超时这些现实条件决定了:你不可能在所有场景都既要强一致、又要高性能、还要高可用。

所以落地时要先想清楚:

  • 这个业务必须强一致吗?
  • 能否接受短时间不一致?
  • 如果失败,是自动回滚,还是异步补偿?
  • 事务链路长度多长,吞吐量要求多高?

常见方案对比

在进入 Seata 之前,先把它放到方案坐标系里看,会更容易做判断。

方案一致性侵入性性能适用场景
2PC/XA强一致较差传统强一致场景、资源支持 XA
Seata AT最终接近强一致,自动回滚较好关系型数据库、本地 SQL 事务场景
Seata TCC强控制、业务可定制核心交易、冻结/确认/取消模型
Saga最终一致中高长事务、流程长、允许补偿
本地消息表 / MQ 事务消息最终一致很好高并发异步链路

我自己的经验是:

  • 核心主链路、数据库更新类事务:优先考虑 Seata AT 或 TCC
  • 流程长、涉及外部系统、回滚代价大:更适合 Saga
  • 高并发异步业务:优先消息最终一致性

这篇文章重点讲 Seata AT 模式,因为它是很多团队的第一落地选择,也是“改造成本最低”的方案。


核心原理

Seata 是一个分布式事务框架,核心角色主要有三个:

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

一次下单事务的执行过程

sequenceDiagram
    participant Client as 用户请求
    participant Order as 订单服务(TM/RM)
    participant Inventory as 库存服务(RM)
    participant Account as 账户服务(RM)
    participant TC as Seata TC
    participant DB1 as 订单库
    participant DB2 as 库存库
    participant DB3 as 账户库

    Client->>Order: 提交下单请求
    Order->>TC: 开启全局事务
    TC-->>Order: 返回 XID
    Order->>DB1: 写订单(分支事务)
    Order->>Inventory: 扣库存(XID透传)
    Inventory->>DB2: 扣库存(分支事务)
    Order->>Account: 扣余额(XID透传)
    Account->>DB3: 扣余额(分支事务)
    alt 全部成功
        Order->>TC: 提交全局事务
        TC-->>Order: 提交成功
    else 任一失败
        Order->>TC: 回滚全局事务
        TC->>DB3: 驱动账户分支回滚
        TC->>DB2: 驱动库存分支回滚
        TC->>DB1: 驱动订单分支回滚
    end

AT 模式到底做了什么?

AT 模式的核心思想可以理解为:

  • 业务代码照常执行本地 SQL
  • Seata 代理数据源,拦截 SQL
  • 在执行前后生成镜像数据(before image / after image)
  • 如果全局事务失败,根据镜像自动生成反向 SQL 回滚

这也是为什么 AT 模式对开发很友好:不需要你手写 Try/Confirm/Cancel,大多数场景直接接管 JDBC 层就行。

AT 模式的关键依赖:undo_log

每个参与事务的数据库里,都需要有一张 undo_log 表,用来记录回滚日志。

flowchart TD
    A[业务SQL执行前] --> B[记录 Before Image]
    B --> C[执行本地事务SQL]
    C --> D[记录 After Image]
    D --> E[写入 undo_log]
    E --> F[本地事务提交]
    F --> G{全局事务结果}
    G -->|提交| H[异步清理 undo_log]
    G -->|回滚| I[根据镜像生成反向SQL]

Seata 事务边界怎么理解?

这里有个特别容易误解的点:
Seata 管的是“全局事务协调”,但真正落地执行的仍然是各服务自己的本地事务”。

也就是说:

  • 每个分支本地事务先提交
  • 全局层面决定最终是“确认有效”还是“反向补偿回滚”

所以它不是传统数据库那种一直锁到最后的严格全局锁模型,而是通过:

  • 本地事务提交
  • 全局锁
  • undo_log 补偿回滚

来实现一种工程上可接受的一致性方案。


架构设计与取舍分析

如果你准备在生产环境上 Seata,我建议先从以下几个问题评估:

1. 业务适不适合 AT?

AT 模式比较适合:

  • MySQL / MariaDB 等关系型数据库
  • 典型的 update / insert / delete
  • 回滚可以通过 SQL 镜像恢复
  • 服务主要是数据库事务,不涉及复杂外部副作用

AT 不太适合:

  • 调用了不可逆的外部接口,比如短信、第三方支付、发券
  • 大事务、长事务
  • 包含复杂 SQL:多表联查更新、存储过程、非标准写法
  • 强依赖高并发热点更新

2. Seata 只是事务层,不是业务兜底层

这个认知很重要。Seata 能帮你解决“数据库层面的分布式提交/回滚”,但对这些问题无能为力:

  • 第三方接口已成功但无法回滚
  • 服务超时后,调用方重复提交
  • 异步通知重复消费
  • 业务幂等没做好导致重复扣减

所以比较稳妥的设计通常是:

  • 数据库一致性:交给 Seata
  • 业务幂等:业务表唯一索引、幂等号、去重表
  • 最终对账:定时校验任务
  • 外部副作用控制:放到事务成功后异步执行

3. 容量估算别忽略 TC

很多人做压测时,只关注业务服务和数据库,却忘了 Seata Server 也是链路核心。

粗略估算要关注:

  • 每秒全局事务数(TPS)
  • 每个全局事务的分支数
  • 平均事务持续时间
  • 回滚比例
  • TC 的 session 存储方式(内存 / db / redis / raft 等,视版本和部署方式而定)

经验上:

  • 事务持续时间越长,TC 持有的会话越多
  • 分支越多,协调开销越大
  • 回滚越频繁,数据库 undo 和锁冲突越明显

一句话建议:分布式事务要短、快、少分支。


实战代码(可运行)

下面用一个简化但可运行的 Spring Boot + Seata AT 示例,演示“订单服务调用库存服务和账户服务”。

为了控制篇幅,示例聚焦关键代码。你可以把它理解为三个 Spring Boot 服务:

  • order-service
  • inventory-service
  • account-service

1. 数据库表结构

订单库

CREATE TABLE IF NOT EXISTS orders (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  user_id BIGINT NOT NULL,
  product_id BIGINT NOT NULL,
  price DECIMAL(10,2) NOT NULL,
  amount INT NOT NULL,
  status VARCHAR(20) NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS undo_log (
  id BIGINT NOT NULL AUTO_INCREMENT,
  branch_id BIGINT NOT NULL,
  xid VARCHAR(128) NOT NULL,
  context VARCHAR(128) NOT NULL,
  rollback_info LONGBLOB NOT NULL,
  log_status INT NOT NULL,
  log_created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  log_modified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  ext VARCHAR(100),
  PRIMARY KEY (id),
  UNIQUE KEY ux_undo_log (xid, branch_id)
);

库存库

CREATE TABLE IF NOT EXISTS inventory (
  product_id BIGINT PRIMARY KEY,
  total INT NOT NULL,
  used INT NOT NULL,
  residue INT NOT NULL
);

INSERT INTO inventory(product_id, total, used, residue)
VALUES (1, 100, 0, 100)
ON DUPLICATE KEY UPDATE total = total;

CREATE TABLE IF NOT EXISTS undo_log (
  id BIGINT NOT NULL AUTO_INCREMENT,
  branch_id BIGINT NOT NULL,
  xid VARCHAR(128) NOT NULL,
  context VARCHAR(128) NOT NULL,
  rollback_info LONGBLOB NOT NULL,
  log_status INT NOT NULL,
  log_created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  log_modified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  ext VARCHAR(100),
  PRIMARY KEY (id),
  UNIQUE KEY ux_undo_log (xid, branch_id)
);

账户库

CREATE TABLE IF NOT EXISTS account (
  user_id BIGINT PRIMARY KEY,
  balance DECIMAL(10,2) NOT NULL,
  frozen DECIMAL(10,2) NOT NULL DEFAULT 0
);

INSERT INTO account(user_id, balance, frozen)
VALUES (1001, 1000.00, 0)
ON DUPLICATE KEY UPDATE balance = balance;

CREATE TABLE IF NOT EXISTS undo_log (
  id BIGINT NOT NULL AUTO_INCREMENT,
  branch_id BIGINT NOT NULL,
  xid VARCHAR(128) NOT NULL,
  context VARCHAR(128) NOT NULL,
  rollback_info LONGBLOB NOT NULL,
  log_status INT NOT NULL,
  log_created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  log_modified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  ext VARCHAR(100),
  PRIMARY KEY (id),
  UNIQUE KEY ux_undo_log (xid, branch_id)
);

2. Maven 依赖

以 Spring Boot 2.x + Spring Cloud Alibaba 常见组合为例:

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

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

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

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
    </dependency>
</dependencies>

3. 订单服务配置

server:
  port: 8081

spring:
  application:
    name: order-service
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/order_db?useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver

seata:
  enabled: true
  application-id: ${spring.application.name}
  tx-service-group: my_test_tx_group
  service:
    vgroup-mapping:
      my_test_tx_group: default
    grouplist:
      default: 127.0.0.1:8091
  registry:
    type: file
  config:
    type: file

库存服务、账户服务只要改服务名、端口和数据库连接即可。


4. Feign/HTTP 调用接口

为了减少依赖,这里直接用 RestTemplate

package com.example.order.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class AppConfig {

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

5. 数据访问层

OrderRepository

package com.example.order.repository;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

@Repository
public class OrderRepository {

    private final JdbcTemplate jdbcTemplate;

    public OrderRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public void createOrder(Long userId, Long productId, Integer amount, Double price) {
        jdbcTemplate.update(
            "INSERT INTO orders(user_id, product_id, price, amount, status) VALUES (?, ?, ?, ?, ?)",
            userId, productId, price, amount, "INIT"
        );
    }

    public void updateStatus(Long userId, Long productId, String status) {
        jdbcTemplate.update(
            "UPDATE orders SET status = ? WHERE user_id = ? AND product_id = ?",
            status, userId, productId
        );
    }
}

InventoryRepository

package com.example.inventory.repository;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

@Repository
public class InventoryRepository {

    private final JdbcTemplate jdbcTemplate;

    public InventoryRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public int deduct(Long productId, Integer amount) {
        return jdbcTemplate.update(
            "UPDATE inventory SET used = used + ?, residue = residue - ? WHERE product_id = ? AND residue >= ?",
            amount, amount, productId, amount
        );
    }
}

AccountRepository

package com.example.account.repository;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.math.BigDecimal;

@Repository
public class AccountRepository {

    private final JdbcTemplate jdbcTemplate;

    public AccountRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public int deduct(Long userId, BigDecimal money) {
        return jdbcTemplate.update(
            "UPDATE account SET balance = balance - ? WHERE user_id = ? AND balance >= ?",
            money, userId, money
        );
    }
}

6. 业务服务实现

订单服务:开启全局事务

package com.example.order.service;

import com.example.order.repository.OrderRepository;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

@Service
public class OrderService {

    private final OrderRepository orderRepository;
    private final RestTemplate restTemplate;

    public OrderService(OrderRepository orderRepository, RestTemplate restTemplate) {
        this.orderRepository = orderRepository;
        this.restTemplate = restTemplate;
    }

    @GlobalTransactional(name = "create-order-tx", rollbackFor = Exception.class)
    public void create(Long userId, Long productId, Integer amount, Double price) {
        orderRepository.createOrder(userId, productId, amount, price);

        String inventoryUrl = String.format(
            "http://127.0.0.1:8082/inventory/deduct?productId=%d&amount=%d",
            productId, amount
        );
        restTemplate.postForObject(inventoryUrl, null, String.class);

        String accountUrl = String.format(
            "http://127.0.0.1:8083/account/deduct?userId=%d&money=%s",
            userId, amount * price
        );
        restTemplate.postForObject(accountUrl, null, String.class);

        orderRepository.updateStatus(userId, productId, "SUCCESS");
    }
}

库存服务:参与分支事务

package com.example.inventory.service;

import com.example.inventory.repository.InventoryRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class InventoryService {

    private final InventoryRepository inventoryRepository;

    public InventoryService(InventoryRepository inventoryRepository) {
        this.inventoryRepository = inventoryRepository;
    }

    @Transactional(rollbackFor = Exception.class)
    public void deduct(Long productId, Integer amount) {
        int updated = inventoryRepository.deduct(productId, amount);
        if (updated == 0) {
            throw new IllegalStateException("库存不足,扣减失败");
        }
    }
}

账户服务:参与分支事务

package com.example.account.service;

import com.example.account.repository.AccountRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;

@Service
public class AccountService {

    private final AccountRepository accountRepository;

    public AccountService(AccountRepository accountRepository) {
        this.accountRepository = accountRepository;
    }

    @Transactional(rollbackFor = Exception.class)
    public void deduct(Long userId, BigDecimal money) {
        int updated = accountRepository.deduct(userId, money);
        if (updated == 0) {
            throw new IllegalStateException("余额不足,扣减失败");
        }
    }
}

7. Controller 暴露接口

订单接口

package com.example.order.controller;

import com.example.order.service.OrderService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderController {

    private final OrderService orderService;

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

    @PostMapping("/order/create")
    public String create(@RequestParam Long userId,
                         @RequestParam Long productId,
                         @RequestParam Integer amount,
                         @RequestParam Double price) {
        orderService.create(userId, productId, amount, price);
        return "ok";
    }
}

库存接口

package com.example.inventory.controller;

import com.example.inventory.service.InventoryService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class InventoryController {

    private final InventoryService inventoryService;

    public InventoryController(InventoryService inventoryService) {
        this.inventoryService = inventoryService;
    }

    @PostMapping("/inventory/deduct")
    public String deduct(@RequestParam Long productId, @RequestParam Integer amount) {
        inventoryService.deduct(productId, amount);
        return "ok";
    }
}

账户接口

package com.example.account.controller;

import com.example.account.service.AccountService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.math.BigDecimal;

@RestController
public class AccountController {

    private final AccountService accountService;

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

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

8. 启动与验证

启动顺序建议:

  1. Seata Server
  2. inventory-service
  3. account-service
  4. order-service

请求测试:

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

成功预期

  • orders 新增一条状态为 SUCCESS
  • inventory.residue 减少 2
  • account.balance 减少 200

失败验证

你可以把账户余额改小一点,比如 50,再次发请求:

UPDATE account SET balance = 50 WHERE user_id = 1001;

再调用:

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

预期:

  • 订单插入会被回滚
  • 库存扣减会被回滚
  • 账户扣减失败
  • 最终三个库数据保持一致

调用链路与事务边界图

这张图适合放到设计评审里,能帮助团队统一认知:谁负责发起事务,谁负责参与,异常由谁兜底。

flowchart LR
    A[客户端请求] --> B[订单服务<br/>GlobalTransactional]
    B --> C[本地订单写入]
    B --> D[调用库存服务]
    B --> E[调用账户服务]
    D --> F[库存本地事务]
    E --> G[账户本地事务]
    C --> H[Seata TC协调]
    F --> H
    G --> H
    H --> I{全局成功?}
    I -->|是| J[全部提交完成]
    I -->|否| K[全部回滚]

常见坑与排查

Seata 在 demo 里通常很好用,但上了真实项目,坑会明显变多。下面这些是我见过、也踩过的高频问题。

1. 全局事务没生效

现象

  • @GlobalTransactional 加了,但下游服务没有加入全局事务
  • 发生异常后只有本地回滚,没有全局回滚

排查点

  1. XID 是否透传
    • HTTP、Feign、Dubbo 是否传递了 Seata 上下文
  2. 数据源是否被 Seata 代理
    • 没代理的数据源,不会记录 undo_log
  3. 下游服务是否接入 Seata
    • 仅调用方接入,参与方没接入,也不行
  4. 代理是否失效
    • 比如自己 new 出来的对象,AOP 不生效

建议

打印当前 XID 看最直接:

import io.seata.core.context.RootContext;

System.out.println("current xid = " + RootContext.getXID());

如果下游拿不到 XID,先别怀疑 Seata,先看 RPC 透传链路。


2. undo_log 表缺失或结构不对

现象

  • 分支事务注册失败
  • 回滚时报错
  • 数据无法恢复

建议

  • 每个参与事务的库都必须有 undo_log
  • 表结构尽量使用官方推荐版本
  • 不要自己随意改字段长度和索引

这个问题很基础,但线上最常见。特别是多环境部署时,有的库建了,有的库没建。


3. 自调用导致事务注解失效

现象

同一个类里:

public void a() {
    b();
}

@GlobalTransactional
public void b() {
}

这样 a() 内部直接调用 b(),AOP 代理不会生效,@GlobalTransactional 失效。

解决办法

  • 事务方法放到独立 Bean
  • 或通过代理对象调用

这是 Spring 事务老问题了,在 Seata 里同样成立。


4. 长事务导致锁冲突严重

现象

  • 数据库更新很慢
  • 全局锁等待
  • 大量事务超时后回滚

原因

AT 模式虽然比 XA 轻,但并不意味着“零成本”。如果你在全局事务里做了这些事:

  • 远程调用很多个服务
  • 查询外部系统
  • 做复杂计算
  • 等用户输入
  • 发消息还等确认

那事务时间一长,锁冲突会急剧增加。

建议

  • 一个全局事务只做必要的核心写操作
  • 非关键逻辑后移到事务外
  • 能异步就异步
  • 控制分支数量

我的经验是:全局事务里不要放“可慢”的事情。


5. 幂等没做好,重试把数据打歪

Seata 解决的是事务协调,不是业务幂等。比如:

  • 订单创建接口超时,客户端重试
  • 网关重放请求
  • MQ 重投

如果没做幂等,你可能会得到:

  • 多条重复订单
  • 多次扣库存
  • 多次扣余额

建议

  • 订单号作为唯一业务键
  • 扣减类操作使用唯一流水号
  • 建立幂等记录表或唯一索引

例如订单表加唯一键:

ALTER TABLE orders ADD COLUMN order_no VARCHAR(64) NOT NULL;
ALTER TABLE orders ADD UNIQUE KEY uk_order_no(order_no);

6. 回滚失败怎么办?

现象

  • TC 判定需要回滚
  • 但分支事务回滚失败
  • 数据卡在异常状态

处理思路

  1. 查 Seata Server 日志
  2. 查应用日志里的 XID
  3. undo_log 是否完整
  4. 看原始数据是否已被非事务链路修改
  5. 必要时人工补偿 + 对账修复

如果业务数据在事务提交后又被别的流程更新,AT 回滚时可能发现镜像校验不通过。这种场景说明:同一份热点数据被多个流程并发修改,设计上就应该重新拆分事务边界。


安全/性能最佳实践

这一部分往往比“怎么接入”更重要,因为决定了系统能不能稳定跑。

安全最佳实践

1. 不把外部不可逆操作放进事务主链路

比如:

  • 发短信
  • 发邮件
  • 调第三方支付确认接口
  • 推送优惠券

这些操作即便和 Seata 放一起,也无法真正回滚。更好的做法是:

  • 事务内只完成核心数据库变更
  • 事务成功后,通过消息或事件异步触发外部动作

2. 做好接口幂等与防重

推荐至少做到:

  • 请求唯一 ID
  • 业务主键唯一约束
  • 重试可识别
  • 防止空回滚、悬挂问题(特别是 TCC 场景)

3. 保护 Seata Server

  • 不要把 TC 暴露到公网
  • 配置访问控制和内网隔离
  • 做好日志脱敏,避免输出敏感业务参数
  • 生产环境开启监控和告警

性能最佳实践

1. 缩短全局事务时间

这是最有效的优化手段,没有之一。

可以把链路拆成两段:

  • 事务内:订单、库存、账户核心写操作
  • 事务外:消息通知、积分发放、营销动作

2. 避免热点行更新

如果所有请求都在更新同一行库存,比如一个爆款商品单库存字段,那竞争会很激烈。

可考虑:

  • 分库分表
  • 库存分段
  • 预扣减 + 异步确认
  • 业务层排队削峰

3. 控制 SQL 复杂度

AT 对标准 SQL 支持较好,但复杂 SQL 会增加解析和回滚成本。

建议:

  • 单表更新优先
  • 避免大批量事务更新
  • 少用复杂联表更新
  • 每次事务只改必要数据

4. 事务超时设置要合理

超时过短:

  • 正常业务也被误杀回滚

超时过长:

  • 锁占用过久
  • 故障恢复慢

建议基于压测结果来定,而不是拍脑袋。一般从核心接口 P99 延迟的数倍开始评估。


一套更稳的落地建议

如果你的团队准备在生产中启用 Seata,我建议按下面顺序推进:

阶段一:只覆盖最核心、最短的链路

先挑一个典型但相对简单的场景,比如:

  • 下单
  • 扣库存
  • 扣余额

不要一上来把营销、积分、券、物流全塞进去。

阶段二:补齐观测能力

至少要有:

  • 全局事务数
  • 成功率 / 回滚率
  • 平均耗时 / P95 / P99
  • 分支事务失败分布
  • TC 连接数、会话数
  • undo_log 清理情况

阶段三:补齐业务兜底

包括:

  • 幂等
  • 对账
  • 补偿任务
  • 人工修复预案
  • 异常订单状态机

阶段四:评估是否升级为 TCC / Saga

当你发现这些问题时,AT 可能就不够了:

  • 外部系统不可回滚
  • 冻结资源模型更合适
  • 事务时间较长
  • 需要更细粒度业务控制

这时不要硬扛,应该考虑 TCC 或 Saga。


边界条件:什么时候不建议用 Seata AT?

最后单独强调一下边界。不是所有分布式事务都该上 Seata AT。

以下情况我一般不建议直接用:

  • 涉及第三方系统,且动作不可逆
  • 超高并发热点更新,锁竞争会非常严重
  • 长流程审批型事务,持续几分钟甚至几小时
  • SQL 很复杂,回滚逻辑难以预测
  • 团队没有基本的监控、对账、补偿能力

这时候更适合:

  • 事件驱动最终一致性
  • 本地消息表
  • TCC
  • Saga
  • 工作流 + 补偿机制

选型的关键不是“哪个最先进”,而是:哪个最符合你的业务代价模型。


总结

Seata 的价值,不在于把分布式事务变得“像单库事务一样简单”,而在于它给了我们一种成本可控、工程可落地的一致性方案。

你可以把这篇文章记成三句话:

  1. AT 模式适合关系型数据库下的短事务核心链路
  2. Seata 解决的是分布式提交/回滚,不替代幂等、对账、补偿
  3. 真正能在线上跑稳的关键,不是注解本身,而是事务边界设计

如果你现在就要落地,我给一个最务实的建议:

  • 先从一个三段式链路试点
  • 只纳入核心写操作
  • 保证所有参与库都有 undo_log
  • 补齐幂等和对账
  • 用压测结果决定超时、并发和事务边界

这样做,虽然不“炫技”,但通常最能在真实项目里跑起来,而且跑得住。


分享到:

上一篇
《Node.js 中基于 Worker Threads 与消息队列的高并发任务处理实战-423》
下一篇
《AI 智能体实战:基于 RAG 与函数调用构建企业内部知识问答系统》