微服务架构下的分布式事务实战:基于 Seata 的一致性设计与落地优化
在单体时代,事务这件事其实不难:一个数据库连接、一段本地事务、一个 commit,世界就安静了。
但一旦进入微服务架构,订单、库存、账户、优惠券分散到不同服务和数据库里,“一次下单”就不再是一个数据库事务,而是一串跨服务调用。
这时候问题就来了:
- 订单创建成功了,但库存扣减失败怎么办?
- 库存扣减成功了,但账户扣款超时怎么办?
- 服务重试后重复执行,数据会不会越扣越少?
- 一致性、性能、可用性,到底该怎么平衡?
如果你正在做交易、支付、履约、营销这些链路,分布式事务几乎绕不过去。本文我会从架构设计 + Seata 原理 + 可运行代码 + 排查经验四个层面,把这件事讲透,重点放在最常见、最容易落地的 Seata AT 模式,并顺带讲清楚什么时候该选它,什么时候不该硬上。
背景与问题
先看一个典型场景:用户提交订单。
这个动作通常会拆成几个服务:
- 订单服务:创建订单
- 库存服务:扣减库存
- 账户服务:扣减余额
- 最终将订单状态改为“已完成”
如果没有分布式事务保护,可能会出现下面这些中间态:
- 订单创建了,但库存没扣成功
- 库存扣了,账户没扣成功
- 请求超时后客户端重试,导致重复下单
- 某个服务成功了,但结果没回传,上游误以为失败又补偿一次
这些问题本质上是:业务动作跨多个资源,缺少统一的提交与回滚协调机制。
为什么本地事务不够
本地事务只能保证单库内原子性。
而微服务通常意味着:
- 多数据库
- 多服务实例
- 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 模式的执行流程
- 入口服务开启全局事务
- 下游分支事务依旧执行本地事务
- Seata 记录每个分支事务对应的前后镜像
- 全部分支成功,则 TC 通知全局提交
- 任一分支失败,则 TC 通知全局回滚
- 回滚时依赖
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-servicestorage-serviceaccount-serviceseata-server- MySQL
下单流程:
- 创建订单,状态为
INIT - 扣减库存
- 扣减账户余额
- 修改订单状态为
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日志定位异常码]
日志重点看什么
建议至少关注三类日志:
- TM 日志:全局事务是否开启、提交、回滚
- RM 日志:分支事务是否注册、回滚是否执行
- TC 日志:全局事务状态流转、异常码
典型关键词:
Begin new global transactionRegister branch successfullyRollback branch transactionGlobal 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 条短链路做试点,比如下单扣库存
- 强制补齐唯一键、幂等、失败注入测试
- 确认
undo_log、数据源代理、XID 透传全部正确 - 压测观察全局锁冲突和事务耗时
- 对热点链路做拆分,别把慢调用塞进全局事务
真正稳定的分布式事务,不是“框架一接就完事”,而是框架能力 + 业务边界 + 工程治理三者一起到位。
Seata 能帮你解决很多问题,但前提是你知道它解决的是哪一类问题,以及它解决不到哪里。