微服务架构中分布式事务的实战方案:基于 Seata 的一致性设计与落地经验
微服务拆开之后,单体时代“一个本地事务包打天下”的日子就结束了。下单、扣库存、冻结余额、生成物流单,往往分散在不同服务、不同库里。业务链路一长,数据一致性问题就会很快冒出来:订单成功了,库存没扣;库存扣了,支付回滚了;消息发了,数据库却没提交。
如果你也在做这类系统,Seata 基本绕不过去。它不是银弹,但在“关系型数据库 + Java 微服务 + 对一致性有明确要求”的场景里,确实是很实用的一套方案。本文我会从架构设计、原理、可运行代码、排障经验、性能与安全边界几个角度,把 Seata 的落地过程讲清楚,尽量像带你走一遍项目实践,而不是只讲概念。
背景与问题
先看一个典型链路:用户提交订单时,需要同时完成几件事:
- 订单服务创建订单
- 库存服务扣减库存
- 账户服务扣减余额
- 成功后订单状态改为“已确认”
这在单体里通常是一个数据库事务。但拆成微服务后,事务边界天然被切断了。
为什么本地事务不够用了
每个服务内部仍然可以用本地事务保证一致性,但服务之间的调用就失去了统一提交/回滚能力。最常见的问题有三类:
- 部分成功:订单写入成功,库存服务超时
- 重试导致重复执行:调用方超时后重试,扣库存扣了两次
- 异步链路难回溯:消息已经投递,业务状态却不完整
很多团队一开始会用“失败补偿 + 人工修单”顶着,但业务量上来后,这种方式成本极高,而且非常依赖人。
分布式事务方案并不只有一种
在做方案前,先明确一点:不是所有业务都需要强一致分布式事务。
常见方案大概是这几类:
| 方案 | 一致性 | 复杂度 | 性能 | 适用场景 |
|---|---|---|---|---|
| 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 模式的核心思路是:
- 拦截业务 SQL
- 在提交前生成 before image 和 after image
- 把回滚信息记录到
undo_log - 分支事务先按本地事务提交
- 全局事务成功则整体提交;失败则利用
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新增一条,状态为 1t_stock.residue减少 2t_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. 全局事务没生效,只有本地事务提交了
现象:
- 下游服务异常了
- 上游服务抛错
- 但订单表已经提交,库存也改了
优先检查:
- 是否真的加了
@GlobalTransactional - 注解是否加在对外调用入口的方法上
- 是否存在同类自调用导致 AOP 失效
- 是否启用了
seata-spring-boot-starter - 数据源是否被 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:
-
超高并发热点写场景
- 比如秒杀扣单行库存
- 全局锁冲突会很痛
-
事务跨度很长
- 包含人工审核、外部回调、分钟级状态流转
- 更适合 Saga 或状态机
-
数据库类型复杂
- 非标准 RDBMS、多种异构存储混用
- AT 适配成本会升高
-
SQL 非常复杂
- 大批量、复杂联表、强依赖自定义语句
- 回滚镜像和兼容性风险大
这时候更适合考虑:
- TCC
- Saga
- Outbox + 消息最终一致
- 业务状态机 + 补偿
落地建议:一个真实可执行的分层思路
如果团队第一次接入分布式事务,我建议按下面节奏来,不要一上来就全链路铺开。
第 1 步:先圈核心链路
只挑最关键的一条,比如:
- 下单
- 扣库存
- 扣余额
先把这条链路跑通,并验证:
- 正常提交
- 异常回滚
- 超时重试
- 下游异常
- 回滚恢复
第 2 步:给接口补幂等
至少补这些:
- 请求唯一号
- 订单号防重
- 扣减接口去重表或业务状态校验
第 3 步:再做监控和压测
不要等到线上才关注锁冲突和回滚重试。
压测时重点看:
- 事务平均耗时
- 高峰时 lock conflict
- TC 负载
- DB 慢 SQL
第 4 步:识别不适合 Seata 的链路
把以下链路逐步移出去:
- 外部三方调用
- 耗时不可控逻辑
- 通知、埋点、异步派生数据
最后会形成一个比较健康的架构:
核心资源变更用 Seata,外围副作用用消息和补偿。
总结
Seata 在微服务分布式事务场景里,最大的价值不是“让你重新回到单体时代”,而是提供了一种可工程化落地的一致性能力。
如果你的系统满足这些条件:
- 主要是 Java 微服务
- 数据落在关系型数据库
- 核心链路要求同步一致
- 希望尽量少改现有业务代码
那么 Seata AT 是很值得优先尝试的方案。
但要记住它的边界:
- 它不替代业务幂等
- 它不适合长事务和超热点写
- 它也不该包住所有外部调用
我的实战建议可以压缩成三句:
- 先用 AT 跑通最核心链路,不要贪大求全
- 把事务做短,把 SQL 做简单,把幂等补完整
- 一旦出现热点冲突或长事务,果断考虑 TCC/Saga/消息补偿
真正稳定的分布式一致性设计,从来不是“选了 Seata 就结束”,而是知道什么时候该用它,什么时候该收手。