回顾:跨服务数据一致性的困境
到上一篇为止,我们系统的可靠性已经上了一个台阶:熔断、降级、限流护着,一个服务挂了不会拖垮整个系统。
但可靠性不等于正确性。考虑一个真实的业务场景:
order-service收到下单请求,在自己的数据库里创建了一条订单记录- 然后调用
payment-service扣款 - 扣款失败
现在怎么办?订单已经入库了,钱没扣成。如果不管,就是一笔坏账。如果手动写代码把订单删掉,那几十个跨服务调用组合起来,补偿代码量会比业务代码还多。
这就是分布式事务要解决的问题:多个服务、多个数据库之间,要么全成功、要么全回滚。
Seata 是什么
Seata(Simple Extensible Autonomous Transaction Architecture)是阿里巴巴开源的分布式事务框架。它提供三种事务模式:
| 模式 | 特点 | 适用场景 |
|---|---|---|
| AT | 自动生成反向 SQL,业务代码零侵入 | 基于关系型数据库的微服务 |
| TCC | 需要手写 Try / Confirm / Cancel 三个接口 | 对一致性要求极高、需要自定义资源 |
| Saga | 长事务,通过正向 + 补偿串联 | 流程长、不能加锁的场景 |
我们这一篇聚焦 AT 模式——对初学者最友好、业务代码改动最少的一种。
AT 模式原理
AT 模式的核心思想是两阶段提交,但它不像传统的 XA 协议那样全程锁资源。它的做法更轻量:
一阶段:执行业务 SQL + 记录 undo_log
order-service执行INSERT INTO orders ...,Seata 自动解析这条 SQL,算出回滚这条 INSERT 需要执行什么 DELETE,把反向 SQL 写入undo_log表payment-service执行UPDATE account SET balance = balance - 100 ...,同样自动生成反向 SQL 写入undo_log- 两个服务的业务数据已经写入了各自的数据库,但事务还没定案
二阶段:提交或回滚
- 提交(正常情况):所有服务的一阶段都成功后,TC(事务协调者)通知所有 RM(资源管理者)提交。提交操作很简单——删除对应的
undo_log记录 - 回滚(任一服务失败):TC 通知所有 RM 执行回滚。回滚操作就是把
undo_log里提前算好的反向 SQL 执行一遍——INSERT 变成 DELETE,UPDATE 变成反向 UPDATE
整个过程中,你写的业务代码里不需要出现任何补偿逻辑。Seata 在你执行 SQL 时偷偷记下了 undo_log,回滚时再拿出来用。
Seata 三角色
Seata 把分布式事务的参与者分成三个角色:
TM (事务发起者)
│
│ 开启/提交/回滚全局事务
▼
TC (事务协调者) ◄──── Seata Server
│
│ 调度分支事务的提交/回滚
▼
RM (资源管理者) ◄──── order-service / payment-service
- TC(Transaction Coordinator):事务协调者,独立部署的 Seata Server,负责维护全局事务状态
- TM(Transaction Manager):事务发起者,加
@GlobalTransactional注解的方法所在的服务 - RM(Resource Manager):资源管理者,参与事务的各个服务实例,负责本地事务和 undo_log
三者协作流程:
TM 开启全局事务 → RM1 执行 SQL + 写 undo_log → RM2 执行 SQL + 写 undo_log → TM 提交/回滚
│
┌───────────────────────┘
▼
TC 通知所有 RM:提交(删 undo_log)
或 回滚(执行反向 SQL)
动手:订单 + 支付分布式事务
环境准备:Seata Server
在 docker-compose.yml 中添加 Seata Server:
seata-server:
image: seataio/seata-server:2.0.0
container_name: seata-server
ports:
- "8091:8091"
- "7091:7091"
environment:
- SEATA_IP=localhost
各个服务引入依赖
order-service 和 payment-service 都加入:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
配置 seata
在两个服务的 application.yml 中加入:
seata:
tx-service-group: default_tx_group
service:
vgroup-mapping:
default_tx_group: default
grouplist:
default: localhost:8091
创建 undo_log 表
在每个服务连接的数据库中执行:
CREATE TABLE IF NOT EXISTS undo_log (
id BIGINT(20) NOT NULL AUTO_INCREMENT,
branch_id BIGINT(20) NOT NULL,
xid VARCHAR(100) NOT NULL,
context VARCHAR(128) NOT NULL,
rollback_info LONGBLOB NOT NULL,
log_status INT(11) NOT NULL,
log_created DATETIME NOT NULL,
log_modified DATETIME NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY ux_undo_log (xid, branch_id)
);
这张表就是 Seata 的「保险柜」——每个本地事务执行时,反向 SQL 就存在这里。
标记全局事务
在 order-service 的创建订单方法上加 @GlobalTransactional:
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private PaymentClient paymentClient;
@GlobalTransactional
public void createOrder(Order order) {
// 本地操作:插入订单
orderMapper.insert(order);
// 远程调用:扣款
paymentClient.deduct(order.getAmount());
}
}
一个注解就够了。正常执行时两个服务的操作都会提交;如果 paymentClient.deduct() 抛异常,Seata 会自动回滚 orderMapper.insert()。
验证
正常提交
发送一个正常的下单请求:
curl -X POST http://localhost:8082/order -d '{"amount": 100}'
检查数据库:orders 表有记录,account 表余额减少了 100。undo_log 表对应记录已清空。
回滚
模拟异常:在 payment-service 中让扣款逻辑抛异常,再发下单请求。
检查数据库:orders 表没有新记录,account 表余额没变。分布式事务回滚成功。
小结与下篇预告
这一篇我们解决了跨服务的原子性问题:
- 认识分布式事务的困境——单服务事务管不了跨服务的数据一致性
- Seata 三种模式,聚焦 AT 模式:业务 SQL + undo_log 自动反向 SQL
- TC / TM / RM 三角色协作模型
- 二阶段提交:一阶段执行业务 SQL + 不改业务代码,二阶段删 undolog 或执行反向 SQL
- 动手:Docker Compose 启动 Seata Server,
@GlobalTransactional一个注解收口全局事务
现在服务已经能跑、能稳、能保证数据一致了。但还有一个问题:当一个请求从 Gateway → order-service → payment-service 一路走下去,如果中途报错了或者慢了,你只能看到一个最终的报错信息,中间环节发生了什么完全黑盒。
下一篇,我们用 Micrometer + Brave + Zipkin 给整个调用链装上追踪系统,让每次请求的全链路都可视化。