Seata 分布式事务原理与实战
Seata是什么
Seata(Simple Extensible Autonomous Transaction Architecture)是阿里开源的一站式分布式事务解决方案,支持 AT / XA / TCC / Saga 四种事务模式。它的核心能力是让跨服务、跨数据库的事务像单库事务一样简单——业务代码几乎不需要改动。
一句话定位: Nacos 管服务发现,Sentinel 管流量防护,Seata 管数据一致性。
为什么需要
微服务架构下,一个业务操作经常跨越多个服务,每个服务有自己的数据库:
没有分布式事务的后果:订单创建成功但库存扣减失败、余额已扣但订单未生成——数据不一致。数据库本地事务无法跨服务,必须有一个协调者来保证"全成功或全回滚"。
核心架构
Seata 定义了三个角色(比 2PC 的协调者/参与者更细致):
各角色职责:
| 角色 | 全称 | 做什么 |
|---|---|---|
| TC | Transaction Coordinator | Seata 服务端,管理全局事务状态,协调分支提交/回滚 |
| TM | Transaction Manager | 业务入口,通过 @GlobalTransactional 开启全局事务,告诉 TC 是 commit 还是 rollback |
| RM | Resource Manager | 参与事务的业务服务,向 TC 注册分支事务,执行本地 SQL 并报告执行结果 |
四种事务模式详解
AT 模式(默认,重点)
AT(Auto Transaction)是 Seata 的默认和最常用模式。它的核心思路:一阶段直接提交本地事务 + 保存 undo log 快照用于回滚。对业务代码零侵入。
AT 的五个关键设计:
| 设计 | 作用 |
|---|---|
| 一阶段提交 | 每个分支事务执行完 SQL 直接 commit,不长时间锁数据库资源(跟 XA 的长期锁定形成关键对比) |
| undo log 快照 | before-image(修改前快照)和 after-image(修改后快照)保存在 undo_log 表中,用于回滚时生成反向 SQL |
| 全局锁 | TC 维护,防止 AT 事务之间的脏写。分支事务提交前先申请全局锁,拿不到则重试,超时则回滚 |
| 数据快照校验 | 回滚时对比当前数据与 after-image,一致则用 before-image 恢复;不一致说明被非 Seata 事务修改过→人工介入 |
| 读未提交隔离 | 默认全局隔离级别是读未提交,性能至上。需要读已提交时用 SELECT FOR UPDATE(会申请全局锁) |
AT 对比 XA 最核心的区别: XA 在一阶段只预留资源(不提交),二阶段才真正提交或回滚,资源锁定时间很长。AT 在一阶段直接提交,二阶段要么删 undo log(提交),要么用 undo log 回滚(回滚),资源锁定时间极短。所以 AT 性能远好于 XA。
XA 模式
完全遵循 2PC 协议。一阶段不提交,二阶段才 commit 或 rollback。强一致,高性能差。
| 优点 | 缺点 |
|---|---|
| 强一致,满足 ACID | 长期锁资源,性能最差 |
| 无代码入侵 | 依赖关系型数据库 |
TCC 模式
Try-Confirm-Cancel 三段式,每个阶段都需要业务代码手动实现。性能最好,侵入性最大。
| 优点 | 缺点 |
|---|---|
| 一阶段提交,无全局锁,性能极致 | 每个阶段都要手写业务代码 |
| 不依赖数据库事务,支持非关系型存储 | 需处理空回滚和业务悬挂问题 |
TCC 的两个关键陷阱:
- 空回滚: Try 阻塞时全局事务已超时,触发了 Cancel。但 Try 还没执行,Cancel 做了不应该做的回滚。解决:记录事务状态,Cancel 前检查 Try 是否已执行。
- 业务悬挂: 空回滚后 Try 才到达,但已无法 Confirm 或 Cancel。解决:Try 执行前检查是否已被 Cancel。
Saga 模式
长事务拆分为多个本地短事务依次提交,失败时按逆序执行补偿操作。
| 优点 | 缺点 |
|---|---|
| 一阶段提交,无锁 | 无隔离性,可能有脏写 |
| 适合长事务、事件驱动 | 补偿逻辑需要手动实现 |
四种模式选型速查
| 模式 | 入侵性 | 一致性 | 性能 | 适用场景 |
|---|---|---|---|---|
| AT | 无 | 最终一致 | 高 | 大多数业务场景,默认选这个 |
| XA | 无 | 强一致 | 低 | 对一致性要求极严,可以接受性能损失 |
| TCC | 高 | 最终一致 | 最高 | 性能是硬要求,且愿意为每个接口写三套逻辑 |
| Saga | 中 | 最终一致 | 高 | 长事务、事件驱动、不适合锁资源的场景 |
新人上手指南:AT 模式完整示例
第一步:启动 Seata Server
# 1. 下载
curl -O https://github.com/apache/incubator-seata/releases/download/v2.0.0/seata-server-2.0.0.zip
unzip seata-server-2.0.0.zip && cd seata
# 2. 配置存储(默认 file 模式,测试可用)
# 生产环境请改为 db 模式:修改 conf/application.yml 中 store.mode=db,并连接 MySQL
# 3. 启动
sh bin/seata-server.sh -p 8091 -h 127.0.0.1
# -p 端口(默认 8091),-h 注册 IP
第二步:在每个参与事务的数据库中创建 undo_log 表
-- AT 模式必须!每个业务数据库都需要这张表
CREATE TABLE IF NOT EXISTS `undo_log`
(
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`branch_id` BIGINT(20) NOT NULL COMMENT '分支事务ID',
`xid` VARCHAR(128) NOT NULL COMMENT '全局事务ID',
`context` VARCHAR(128) NOT NULL COMMENT '上下文',
`rollback_info` LONGBLOB NOT NULL COMMENT '回滚信息(before/after image)',
`log_status` INT(11) NOT NULL COMMENT '状态',
`log_created` DATETIME NOT NULL COMMENT '创建时间',
`log_modified` DATETIME NOT NULL COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT = 'AT事务undo日志表';
第三步:添加依赖与配置
<!-- 每个参与事务的服务都要加 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
# application.yml(每个服务各自配置自己的数据源和 Seata)
spring:
application:
name: order-service
datasource:
url: jdbc:mysql://localhost:3306/order_db?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
seata:
application-id: ${spring.application.name}
tx-service-group: my_tx_group # 事务分组名,需与 Seata Server 配置一致
registry:
type: nacos # Seata 用 Nacos 做注册中心
nacos:
server-addr: 127.0.0.1:8848
namespace: ""
group: SEATA_GROUP
config:
type: nacos # 配置也放 Nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: ""
group: SEATA_GROUP
service:
vgroup-mapping:
my_tx_group: default # 事务分组→Seata 集群名映射
data-source-proxy-mode: AT # 启用 AT 数据源代理(自动生成 undo log)
第四步:在入口方法上加 @GlobalTransactional
业务场景: 下单扣库存扣余额,三个操作分属三个服务,必须同时成功或同时失败。
@FeignClient(name = "order-service")
public interface OrderClient {
@PostMapping("/order/create")
Result create(@RequestBody OrderReq req);
}
@FeignClient(name = "stock-service")
public interface StockClient {
@PostMapping("/stock/deduct")
Result deduct(@RequestBody StockReq req);
}
@FeignClient(name = "account-service")
public interface AccountClient {
@PostMapping("/account/debit")
Result debit(@RequestBody AccountReq req);
}
@Service
public class BusinessService {
@Autowired
private OrderClient orderClient;
@Autowired
private StockClient stockClient;
@Autowired
private AccountClient accountClient;
/**
* 下单主方法。三处调用的任何一个失败都会导致全部回滚。
* 对调用方来说,这个方法就像本地事务一样——要么全成,要么全回滚。
*/
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
public Result purchase(String userId, String commodityCode, int count) {
// 1. 扣库存
stockClient.deduct(new StockReq(commodityCode, count));
// 2. 扣余额
accountClient.debit(new AccountReq(userId, count * 100));
// 3. 创建订单
orderClient.create(new OrderReq(userId, commodityCode, count));
return Result.ok("下单成功");
}
}
关于 @GlobalTransactional 的几个细节:
rollbackFor = Exception.class:默认只回滚 RuntimeException,一定要显式设为 Exception 才能捕获所有异常- 调用链路中的 Feign 调用会自动透传
xid(全局事务 ID),不需要手动处理 - 如果某个分支不想要 Seata 管理,在 FeignClient 的 fallback 中自行处理即可
验证
# 正常调用:三个表都更新
curl "http://localhost:8080/business/purchase?userId=U001&commodityCode=C001&count=2"
# → 订单表插入一条、库存减2、账户扣200
# 异常场景:在 StockService 中故意抛异常
curl "http://localhost:8080/business/purchase?userId=U001&commodityCode=C001&count=999999"
# → 库存不足抛异常 → 全部回滚,数据库无变化
# 验证:undo_log 表中能看到回滚记录
常见问题
1. @GlobalTransactional 不生效,数据没回滚
- 确认
seata.data-source-proxy-mode=AT已配置(否则 Seata 不会拦截数据源生成 undo log) - 确认
undo_log表已在每个业务数据库中创建 - 确认 Feign 调用链路上的每个服务都正确引入了配置
- Seata Server 没启动或连通不了:看日志
Could not found global transaction xid
2. undo_log 表缺失
AT 模式强制要求每个业务数据库都有一张 undo_log 表。缺少时一阶段正常提交,但二阶段回滚因无快照而无法执行,导致数据不一致。
3. 全局锁等待超时
多个全局事务同时操作同一行数据时,后到的事务拿不到全局锁,重试超时(默认 30s)后抛 LockConflictException,事务回滚。
常见原因: 高并发热点数据(如秒杀同一商品),业务 SQL 长时间锁表。
对策: 拆分热点、异步扣减、或调大 seata.client.rm.lock.retryInterval 和重试次数。
4. AT 默认读未提交,读到中间状态
AT 默认全局隔离级别是读未提交。需要读已提交时用 @GlobalLock + SELECT FOR UPDATE 申请全局锁。
@GlobalLock
@Transactional
public List<Order> queryUnpaidOrders() {
return orderMapper.selectForUpdate();
}
面试题
Q1:Seata AT 模式的一阶段和二阶段分别做了什么?与 XA 的本质区别是什么?
参考答案:
AT 一阶段:TM 向 TC 开启全局事务获取 xid。每个 RM 执行业务 SQL 前,Seata 代理数据源拦截 SQL 执行,自动生成 before-image 和 after-image 写入 undo_log 表。然后 RM 注册分支事务到 TC,直接提交本地事务。
AT 二阶段:全部成功则各 RM 异步删除 undo_log。任意失败则 RM 根据 undo_log 中的 before-image 生成反向 SQL 回滚。回滚前校验当前数据是否等于 after-image,不一致则报人工介入。
与 XA 的本质区别:
| AT | XA | |
|---|---|---|
| 一阶段 | 直接提交,释放资源 | 只 prepare,不提交,锁定资源 |
| 二阶段提交 | 删 undo log | 真正 commit |
| 二阶段回滚 | 用 before-image 反向恢复 | 真正 rollback |
| 资源锁定时间 | 极短 | 长 |
| 一致性 | 最终一致 | 强一致 |
根本差异: XA 用锁定资源来保一致性,AT 用数据快照 + 补偿来保一致性。这是 Seata AT 性能远超 XA 的原因。
Q2:AT 模式的全局锁和数据快照校验各自解决了什么问题?
参考答案:
这俩是 AT 安全性的双层保障,解决的是不同维度的脏写问题。
全局锁防止 Seata 事务间的脏写: AT 一阶段提交后数据已经可见,此时如果另一个 AT 事务修改同一行数据后回滚,会把前一个事务已经提交的数据也覆盖掉。全局锁确保只有持有锁的事务能执行 SQL,其他 AT 事务必须等锁释放。这样两个 AT 事务之间不会互相脏写。
数据快照校验防止非 Seata 事务的脏写: 但全局锁管不到直接通过数据库客户端手动修改数据的场景。所以 AT 在回滚时会对比当前数据与 after-image。如果不一致,说明有非 Seata 操作改过这行数据。此时 Seata 不会盲目覆盖,而是记录异常日志交由人工处理。
所以完整的安全链条是:全局锁(防 AT 间脏写)+ 数据快照校验(防外部脏写)。缺一不可。
Q3:TCC 为什么会引入空回滚和业务悬挂?怎么解决?
参考答案:
空回滚和业务悬挂都源于同一个根因:TCC 的三个阶段(Try / Confirm / Cancel)是异步的分布式调用,它们的执行顺序可能因为网络超时、线程阻塞而和预期不一致。
空回滚:Try 请求因网络延迟未到达,全局事务已经超时触发了 Cancel。Cancel 执行时发现没有资源需要释放,但它又必须正常返回(否则 TM 会认为取消失败)。这就产生了"空取消"。
业务悬挂:空回滚之后,Try 请求才姗姗来迟。它预留了资源,但全局事务已经结束了,这些资源永远不会被 Confirm 也不会被 Cancel,永远挂在那里。
解决方案:在每个业务数据库中维护一张 TCC 事务状态表,记录每个事务 ID 的执行阶段(Init / Trying / Cancelled / Confirmed)。三个方法都先查询状态再决定是否执行:
- Cancel 执行前:状态是 Init → 标记 Cancelled,直接 return(空回滚)
- Try 执行前:状态是 Cancelled → 跳过执行,避免悬挂
- Try 执行后:状态为 Trying
- Confirm 前:状态必须是 Trying
这个模式通常抽象为一个工具类或 AOP 切面,避免每个 TCC 接口都重复写。这也是 TCC 侵入性高的一个体现——AT 把这层的处理全自动了。
延伸阅读: Seata 官方文档 https://seata.apache.org/zh-cn/docs/overview/what-is-seata/ ,AT 模式的源码分析推荐看"Seata AT 模式源码解析"系列,特别是 DataSourceProxy 如何拦截 SQL 生成 undo log 的细节。

浙公网安备 33010602011771号