微服务架构中分布式事务的实战方案:基于 Seata 的一致性设计与落地指南
在单体应用里,一个数据库事务就能兜住“下单、扣库存、扣余额、生成物流单”这一整串操作;可一旦拆成微服务,这些动作分散到不同服务、不同数据库,原来那句简单的 @Transactional 就不够用了。
很多团队第一次碰到分布式事务,往往会在两种极端之间摇摆:
- 要么强行追求“像单库事务一样绝对一致”,结果架构复杂、性能很差;
- 要么干脆只讲最终一致性,但缺少补偿、对账、幂等等配套设计,线上数据越跑越歪。
这篇文章我想从工程落地的角度,带你把 Seata 这件事讲透:什么时候用、怎么用、代码怎么写、坑怎么排、边界怎么守。如果你已经有 Spring Cloud / Spring Boot 微服务基础,这篇内容应该能直接用于项目设计。
背景与问题
先看一个很典型的业务链路:电商下单。
- 订单服务创建订单
- 库存服务扣减库存
- 账户服务冻结或扣减余额
- 全部成功后,订单状态改为“已确认”
如果每个服务都有自己的数据库,那么一个用户下单动作,实际上变成了多个本地事务的组合。问题在于:
- 订单写成功了,但库存扣减失败;
- 库存扣了,账户没扣成功;
- 某个服务成功执行了,但调用方网络超时,不知道到底要不要重试;
- 服务重试后,重复扣库存或重复扣钱。
本质问题是什么?
分布式事务本质上是在解决两个问题:
- 跨多个资源的一致性
- 异常场景下的可恢复性
而 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. 启动与验证
启动顺序建议:
- Seata Server
- inventory-service
- account-service
- order-service
请求测试:
curl -X POST "http://127.0.0.1:8081/order/create?userId=1001&productId=1&amount=2&price=100"
成功预期
orders新增一条状态为SUCCESSinventory.residue减少 2account.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加了,但下游服务没有加入全局事务- 发生异常后只有本地回滚,没有全局回滚
排查点
- XID 是否透传
- HTTP、Feign、Dubbo 是否传递了 Seata 上下文
- 数据源是否被 Seata 代理
- 没代理的数据源,不会记录 undo_log
- 下游服务是否接入 Seata
- 仅调用方接入,参与方没接入,也不行
- 代理是否失效
- 比如自己 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 判定需要回滚
- 但分支事务回滚失败
- 数据卡在异常状态
处理思路
- 查 Seata Server 日志
- 查应用日志里的 XID
- 看
undo_log是否完整 - 看原始数据是否已被非事务链路修改
- 必要时人工补偿 + 对账修复
如果业务数据在事务提交后又被别的流程更新,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 的价值,不在于把分布式事务变得“像单库事务一样简单”,而在于它给了我们一种成本可控、工程可落地的一致性方案。
你可以把这篇文章记成三句话:
- AT 模式适合关系型数据库下的短事务核心链路
- Seata 解决的是分布式提交/回滚,不替代幂等、对账、补偿
- 真正能在线上跑稳的关键,不是注解本身,而是事务边界设计
如果你现在就要落地,我给一个最务实的建议:
- 先从一个三段式链路试点
- 只纳入核心写操作
- 保证所有参与库都有
undo_log - 补齐幂等和对账
- 用压测结果决定超时、并发和事务边界
这样做,虽然不“炫技”,但通常最能在真实项目里跑起来,而且跑得住。