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

《微服务架构下的分布式事务实战:基于 Seata 的一致性设计与落地优化》

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

微服务架构下的分布式事务实战:基于 Seata 的一致性设计与落地优化

在单体时代,事务这件事其实不难:一个数据库连接、一段本地事务、一个 commit,世界就安静了。
但一旦进入微服务架构,订单、库存、账户、优惠券分散到不同服务和数据库里,“一次下单”就不再是一个数据库事务,而是一串跨服务调用。

这时候问题就来了:

  • 订单创建成功了,但库存扣减失败怎么办?
  • 库存扣减成功了,但账户扣款超时怎么办?
  • 服务重试后重复执行,数据会不会越扣越少?
  • 一致性、性能、可用性,到底该怎么平衡?

如果你正在做交易、支付、履约、营销这些链路,分布式事务几乎绕不过去。本文我会从架构设计 + Seata 原理 + 可运行代码 + 排查经验四个层面,把这件事讲透,重点放在最常见、最容易落地的 Seata AT 模式,并顺带讲清楚什么时候该选它,什么时候不该硬上。


背景与问题

先看一个典型场景:用户提交订单。

这个动作通常会拆成几个服务:

  1. 订单服务:创建订单
  2. 库存服务:扣减库存
  3. 账户服务:扣减余额
  4. 最终将订单状态改为“已完成”

如果没有分布式事务保护,可能会出现下面这些中间态:

  • 订单创建了,但库存没扣成功
  • 库存扣了,账户没扣成功
  • 请求超时后客户端重试,导致重复下单
  • 某个服务成功了,但结果没回传,上游误以为失败又补偿一次

这些问题本质上是:业务动作跨多个资源,缺少统一的提交与回滚协调机制。

为什么本地事务不够

本地事务只能保证单库内原子性
而微服务通常意味着:

  • 多数据库
  • 多服务实例
  • RPC 调用
  • 消息队列异步链路
  • 网络抖动和超时

这几个因素叠加后,事务边界天然被打散。

常见解决思路

分布式事务并不是只有 Seata 一条路。实际架构里常见有三类:

方案一致性性能复杂度适用场景
本地消息表 / 最终一致性最终一致允许延迟一致的业务
TCC强一致/准实时核心资金、库存预留
Seata AT强一致(数据库层)低~中关系型数据库、快速接入

如果你的业务是“订单、库存、账户”这类典型 CRUD 型操作,数据库是 MySQL,且希望尽量少改业务代码,那 Seata AT 往往是一个很现实的选择。


方案对比与取舍分析

在进入 Seata 前,先把边界条件说清楚。很多团队不是不会用 Seata,而是把它用到了不适合的地方。

Seata AT 适合什么

Seata AT 模式的优点很明显:

  • 业务代码侵入性低
  • 基于代理数据源和 undo log 自动回滚
  • 接入成本相对低
  • 对 Spring Cloud / Dubbo 生态支持较好

适合:

  • 关系型数据库事务
  • 以更新单行为主的业务
  • 对一致性要求高于吞吐极限
  • 可以接受全局锁带来的部分性能损耗

Seata AT 不适合什么

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

  • 热点行更新极其频繁
  • 长事务,跨很多服务且耗时明显
  • 大批量更新/复杂 SQL
  • 非关系型数据库为主
  • 强依赖异步事件流,不希望同步阻塞

这时更适合考虑:

  • Saga:长流程、可补偿
  • TCC:核心资源预留式控制
  • 事件驱动最终一致:吞吐优先

一句话总结:AT 模式适合“短、快、标准化”的数据库事务,不适合“重、长、热点强冲突”的链路。


核心原理

Seata 的角色可以先记住四个:

  • TC:Transaction Coordinator,事务协调器
  • TM:Transaction Manager,事务管理器
  • RM:Resource Manager,资源管理器
  • 业务服务:订单、库存、账户等

在 Spring Boot 项目里,你通常写的是业务代码和 @GlobalTransactional,真正的全局事务控制由 Seata 在底层完成。

AT 模式的执行流程

  1. 入口服务开启全局事务
  2. 下游分支事务依旧执行本地事务
  3. Seata 记录每个分支事务对应的前后镜像
  4. 全部分支成功,则 TC 通知全局提交
  5. 任一分支失败,则 TC 通知全局回滚
  6. 回滚时依赖 undo_log 恢复数据

下面这张图能把主流程串起来。

flowchart LR
    A[用户发起下单] --> B[订单服务 TM 开启全局事务]
    B --> C[订单服务写订单 RM分支事务]
    C --> D[库存服务扣库存 RM分支事务]
    D --> E[账户服务扣余额 RM分支事务]
    E --> F{全部成功?}
    F -- 是 --> G[TC 提交全局事务]
    F -- 否 --> H[TC 回滚全局事务]
    H --> I[各分支根据 undo_log 回滚]

Seata AT 为什么能回滚

Seata AT 最关键的能力是:在业务 SQL 执行时,自动生成数据前镜像和后镜像,并写入 undo_log

比如库存扣减:

update storage set count = count - 1 where product_id = 1 and count >= 1;

Seata 在执行过程中会做几件事:

  • 解析 SQL
  • 查询更新前数据,形成 before image
  • 执行业务 SQL
  • 查询更新后数据,形成 after image
  • 写入 undo_log
  • 向 TC 注册分支事务

如果全局失败,就利用 before image 反向恢复。

全局锁解决什么问题

仅仅能回滚还不够,还要避免脏写。

举个例子:

  • 全局事务 A 正在扣减库存
  • 普通本地事务 B 同时也改了同一行库存
  • A 之后回滚,就可能把 B 的结果覆盖掉

所以 Seata 需要引入全局锁,确保分支事务提交前,相关数据行被全局事务保护。

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

    Client->>Order: 创建订单
    Order->>TC: 开启全局事务
    TC-->>Order: 返回 XID

    Order->>Order: 本地事务 + 注册分支
    Order->>Storage: 扣减库存(XID透传)
    Storage->>TC: 注册分支事务
    Storage->>Storage: 执行SQL/写undo_log/申请全局锁

    Storage-->>Order: 成功
    Order->>Account: 扣减余额(XID透传)
    Account->>TC: 注册分支事务
    Account->>Account: 执行SQL/写undo_log/申请全局锁

    alt 全部成功
        Order->>TC: 提交全局事务
        TC-->>Order: 提交成功
    else 任一失败
        Order->>TC: 回滚全局事务
        TC->>Account: 回滚分支
        TC->>Storage: 回滚分支
        TC->>Order: 回滚分支
    end

XID 是怎么传的

XID 可以理解成“这笔全局事务的身份证”。
它需要在服务间调用链路中透传,否则下游服务根本不知道自己在全局事务里。

常见透传方式:

  • Spring Cloud Alibaba:自动透传较方便
  • OpenFeign:需要确认 Seata 相关拦截器生效
  • Dubbo:官方支持较成熟
  • 自定义 HTTP 调用:要自己传递请求头

我踩过一个很典型的坑:本地调试时订单服务加了 @GlobalTransactional,但库存服务始终没加入全局事务,最后发现是自定义 Feign 配置把 Seata 的拦截器链覆盖掉了。


实战架构设计

本文用一个最小可运行案例来演示:

  • order-service
  • storage-service
  • account-service
  • seata-server
  • MySQL

下单流程:

  1. 创建订单,状态为 INIT
  2. 扣减库存
  3. 扣减账户余额
  4. 修改订单状态为 SUCCESS

数据模型

订单表

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_storage` (
  `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 回滚日志表

每个参与 AT 模式的库都要有 undo_log

CREATE TABLE `undo_log` (
  `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` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  `ext` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`branch_id`, `xid`)
);

实战代码(可运行)

下面用 Spring Boot + OpenFeign + Seata 做一个精简示例。代码省略了部分样板配置,但核心逻辑是完整可运行的。

1. 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.6.1</version>
    </dependency>

    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.1</version>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

2. 订单服务配置

server:
  port: 2001

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: root
    driver-class-name: com.mysql.cj.jdbc.Driver

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

3. 数据源代理配置

新版本 starter 大多可以自动代理,但我仍然建议确认代理是否生效。很多“看起来接入了,实际上没回滚”的问题都卡在这里。

package com.example.order.config;

import io.seata.rm.datasource.DataSourceProxy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

@Configuration
public class DataSourceProxyConfig {

    @Bean
    public DataSourceProxy dataSourceProxy(DataSource dataSource) {
        return new DataSourceProxy(dataSource);
    }
}

4. Feign 调用接口

package com.example.order.feign;

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 = "storage-service", url = "http://127.0.0.1:2002")
public interface StorageFeignClient {

    @PostMapping("/storage/decrease")
    String decrease(@RequestParam("productId") Long productId,
                    @RequestParam("count") Integer count);
}

@FeignClient(name = "account-service", url = "http://127.0.0.1:2003")
interface AccountFeignClient {

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

5. 订单服务核心事务

这里是重点:把整个业务流程放进 @GlobalTransactional

package com.example.order.service;

import com.example.order.feign.AccountFeignClient;
import com.example.order.feign.StorageFeignClient;
import com.example.order.mapper.OrderMapper;
import com.example.order.model.Order;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;

@Service
@RequiredArgsConstructor
public class OrderService {

    private final OrderMapper orderMapper;
    private final StorageFeignClient storageFeignClient;
    private final AccountFeignClient accountFeignClient;

    @GlobalTransactional(name = "create-order-tx", rollbackFor = Exception.class)
    public void create(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);

        storageFeignClient.decrease(productId, count);
        accountFeignClient.decrease(userId, money);

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

6. 订单 Mapper

package com.example.order.mapper;

import com.example.order.model.Order;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;

@Mapper
public interface OrderMapper {

    @Insert("INSERT INTO t_order(user_id, product_id, count, money, status) " +
            "VALUES(#{userId}, #{productId}, #{count}, #{money}, #{status})")
    int insert(Order order);

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

7. 库存服务逻辑

package com.example.storage.service;

import com.example.storage.mapper.StorageMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class StorageService {

    private final StorageMapper storageMapper;

    @Transactional(rollbackFor = Exception.class)
    public void decrease(Long productId, Integer count) {
        int updated = storageMapper.decrease(productId, count);
        if (updated <= 0) {
            throw new RuntimeException("库存不足,扣减失败");
        }
    }
}
package com.example.storage.mapper;

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

@Mapper
public interface StorageMapper {

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

8. 账户服务逻辑

这里我故意留一个开关,用于模拟异常,方便你验证回滚。

package com.example.account.service;

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

import java.math.BigDecimal;

@Service
@RequiredArgsConstructor
public class AccountService {

    private final AccountMapper accountMapper;

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

        if (mockFail) {
            throw new RuntimeException("模拟账户服务异常");
        }
    }
}
package com.example.account.mapper;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
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(@Param("userId") Long userId, @Param("money") BigDecimal money);
}

9. Controller 测试入口

package com.example.order.controller;

import com.example.order.service.OrderService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.math.BigDecimal;

@RestController
@RequiredArgsConstructor
public class OrderController {

    private final OrderService orderService;

    @GetMapping("/order/create")
    public String create(@RequestParam Long userId,
                         @RequestParam Long productId,
                         @RequestParam Integer count,
                         @RequestParam BigDecimal money) {
        orderService.create(userId, productId, count, money);
        return "success";
    }
}

10. 验证步骤

先准备初始化数据:

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

正常请求:

curl "http://127.0.0.1:2001/order/create?userId=1&productId=1&count=2&money=100"

你应当看到:

  • 订单新增成功
  • 库存 residue 减少
  • 账户 residue 减少
  • 订单状态变成 1

如果你让账户服务抛异常,则应当看到:

  • 订单回滚
  • 库存回滚
  • 账户回滚

这就是 Seata AT 最核心的价值:让跨服务数据库写操作具备统一回滚能力。


一致性设计中的关键细节

光能跑通不够,真正上线前,一定要把下面几个设计点补齐。

1. 幂等性不能丢给 Seata

Seata 负责的是事务协调,不负责业务天然幂等。

比如客户端超时重试,下单接口可能重复提交。
如果你没有业务唯一键,Seata 也救不了你。

建议:

  • 订单号使用业务唯一键
  • 接口引入 requestId
  • 对重复请求做防重表或唯一索引控制
ALTER TABLE t_order ADD COLUMN order_no VARCHAR(64) NOT NULL;
ALTER TABLE t_order ADD UNIQUE KEY uk_order_no(order_no);

2. 空回滚与悬挂要识别

虽然空回滚、悬挂问题在 TCC 模式更典型,但在复杂链路中你也要有这个意识:
调用是否真正落库成功,不要只看接口返回。

尤其是网络超时场景:

  • 下游其实成功了,但响应丢了
  • 上游误判失败,发起补偿
  • 业务侧如果没有幂等保护,就容易重复操作

3. 全局事务不要包住慢调用

我很不建议把下面这些放进全局事务:

  • 调用第三方支付
  • 调用短信/邮件
  • 大文件上传
  • 大批量报表写入

因为全局事务一旦拉长:

  • 锁持有时间上升
  • 冲突概率上升
  • TC 压力变大
  • 回滚成本变高

比较稳妥的做法是:

  • 核心数据库变更放在全局事务里
  • 外部副作用操作走事务后事件或消息通知

常见坑与排查

这一节是我觉得最值钱的部分。很多 Seata 问题不是原理不会,而是线上一出故障不知从哪下手。

坑 1:没创建 undo_log

现象:

  • 分支事务执行了
  • 全局回滚时报错
  • 数据没有按预期恢复

排查:

SHOW TABLES;
SELECT * FROM undo_log;

如果参与事务的库里没有 undo_log,AT 模式回滚就没有依据。

坑 2:数据源没被代理

现象:

  • @GlobalTransactional 生效了
  • 但某个服务始终不回滚
  • Seata 控制台看不到该服务分支注册

排查要点:

  • 是否使用了 DataSourceProxy
  • 是否存在多数据源配置覆盖
  • ORM 框架是否绕过了代理数据源

我遇到过一个案例:项目引入动态数据源后,最终真正执行 SQL 的不是 Seata 代理后的数据源,结果所有分支看起来都正常,实际上根本没纳管。

坑 3:XID 没透传

现象:

  • 订单服务开启了全局事务
  • 下游库存、账户服务按本地事务提交
  • 整体失败时只有订单回滚

排查日志时重点看 XID:

xid=192.168.1.10:8091:1234567890

如果下游日志中没有 XID,说明它没加入全局事务。
重点检查:

  • Feign 拦截器是否生效
  • 自定义 RestTemplate 是否透传请求头
  • 异步线程是否做了上下文传递

坑 4:SQL 不适配或回滚失败

AT 模式依赖 SQL 解析和镜像生成,因此并不是所有 SQL 都适合。

高风险 SQL 包括:

  • 复杂联表更新
  • 子查询更新
  • 非标准写法
  • 大批量条件更新

建议:

  • 核心链路尽量使用简单、单表、主键/唯一键更新
  • 先在测试环境做失败注入验证
  • 检查 Seata 版本对 SQL 解析的支持情况

坑 5:全局锁冲突导致吞吐下降

现象:

  • 高峰期 RT 飙升
  • 部分请求报全局锁等待超时
  • 数据库本身并不慢,但接口卡住

这往往不是数据库性能问题,而是同一热点资源被高频并发修改
典型如:

  • 单商品库存热点扣减
  • 单账户余额频繁写
  • 单优惠券模板集中抢兑

此时要考虑:

  • 业务拆分热点
  • 引入库存分桶
  • 减少事务粒度
  • 核心链路从 AT 切到预留式 TCC 或消息化削峰

排查路径建议

如果线上出问题,我建议按下面顺序排:

flowchart TD
    A[事务失败或数据不一致] --> B{下游是否收到XID}
    B -- 否 --> C[检查RPC透传/拦截器]
    B -- 是 --> D{分支是否注册到TC}
    D -- 否 --> E[检查数据源代理/Seata配置]
    D -- 是 --> F{undo_log是否生成}
    F -- 否 --> G[检查SQL执行链路/代理失效]
    F -- 是 --> H{是否存在锁冲突或SQL不兼容}
    H -- 是 --> I[优化SQL/降低热点/缩短事务]
    H -- 否 --> J[查看TC/RM日志定位异常码]

日志重点看什么

建议至少关注三类日志:

  1. TM 日志:全局事务是否开启、提交、回滚
  2. RM 日志:分支事务是否注册、回滚是否执行
  3. TC 日志:全局事务状态流转、异常码

典型关键词:

  • Begin new global transaction
  • Register branch successfully
  • Rollback branch transaction
  • Global lock acquire failed

安全/性能最佳实践

Seata 讲一致性,但落地时不能只盯着功能正确,还要关心安全和性能。

安全最佳实践

1. 不要把事务上下文暴露给不可信边界

XID 本质上是内部事务上下文,不应该被外部客户端直接伪造传入。
建议:

  • XID 仅在内部服务链路透传
  • API 网关清理外部来路中的 Seata 相关头
  • 内外网调用链做边界隔离

2. 严格控制回滚日志数据访问

undo_log 里可能包含回滚镜像,属于敏感运行数据。

建议:

  • 业务账号最小权限
  • 限制非 DBA 直接读取
  • 做库级备份与审计
  • 避免把 undo_log 内容输出到应用日志

性能最佳实践

1. 保持事务短小

经验上,AT 模式最怕“又长又慢”。

建议控制:

  • 一个全局事务只做必要写操作
  • 避免人机交互等待
  • 避免远程慢调用
  • 避免大事务循环更新

2. 使用索引精确命中

库存和账户扣减 SQL 应该命中唯一键或高选择性索引。
否则:

  • 前后镜像扫描成本上升
  • 锁范围扩大
  • 回滚变慢

例如:

CREATE UNIQUE INDEX uk_product_id ON t_storage(product_id);
CREATE UNIQUE INDEX uk_user_id ON t_account(user_id);

3. 监控 TC 与锁冲突指标

上线后至少监控这些指标:

  • 全局事务成功率
  • 全局事务平均耗时
  • 分支事务注册失败数
  • 回滚次数
  • 全局锁等待超时数
  • undo_log 增长速度

4. 容量估算要看峰值写冲突

AT 模式容量评估不能只看 QPS,还要看:

  • 同一资源的并发更新密度
  • 平均事务时长
  • 回滚比例
  • 热点行占比

一个简单经验:

如果 80% 的写请求都打在极少数热点行上,AT 的锁冲突成本会迅速放大。

这种场景要尽早做业务拆分,不要等线上 RT 飙升了才想起改架构。


什么时候应该从 AT 升级到其他模式

Seata AT 不是银弹。下面这些信号出现时,说明你该重新评估方案了:

  • 单次事务跨 5 个以上服务,耗时持续偏高
  • 热点资源写冲突严重
  • 某些动作无法通过数据库回滚恢复
  • 外部系统调用占事务耗时大头
  • 回滚逻辑需要业务语义,而不是简单数据还原

这时可以考虑:

  • TCC:对 Try / Confirm / Cancel 语义明确的核心资源
  • Saga:长事务、可补偿流程
  • 本地消息表 + MQ:吞吐优先的最终一致方案

我的建议是:
把 Seata AT 用在最适合它的 20% 核心链路上,而不是试图让它接管所有分布式一致性问题。


总结

如果把分布式事务这件事说得朴素一点,它其实是在回答一个问题:

一次业务动作拆到多个服务后,怎么保证“要么都成功,要么都撤回”?

Seata AT 给出的答案是:

  • 通过 @GlobalTransactional 开启全局事务
  • 通过数据源代理接管本地事务
  • 通过 undo_log 实现自动回滚
  • 通过全局锁避免脏写

它的优势在于:

  • 接入成本相对低
  • 适合关系型数据库场景
  • 对订单、库存、账户这类 CRUD 业务很友好

但它也有明确边界:

  • 不适合长事务
  • 不适合高热点冲突
  • 不适合复杂 SQL 和大批量操作
  • 不替代业务幂等、防重、补偿设计

如果你准备在项目里落地,我建议按这个顺序推进:

  1. 先选 1 条短链路做试点,比如下单扣库存
  2. 强制补齐唯一键、幂等、失败注入测试
  3. 确认 undo_log、数据源代理、XID 透传全部正确
  4. 压测观察全局锁冲突和事务耗时
  5. 对热点链路做拆分,别把慢调用塞进全局事务

真正稳定的分布式事务,不是“框架一接就完事”,而是框架能力 + 业务边界 + 工程治理三者一起到位。
Seata 能帮你解决很多问题,但前提是你知道它解决的是哪一类问题,以及它解决不到哪里。


分享到:

上一篇
《微服务架构中基于服务网格的灰度发布与流量治理实战-493》
下一篇
《Java开发踩坑实录:ThreadLocal 在线程池中的内存泄漏与上下文串扰排查实践》