分布式事务
问题
分布式事务有哪些解决方案?2PC、TCC、Saga 各有什么特点?Seata 是如何实现分布式事务的?
答案
为什么需要分布式事务
微服务架构下,一个业务操作可能跨多个服务和数据库。单机事务无法保证跨节点的数据一致性。
如果订单创建成功,但库存扣减失败,就需要分布式事务来保证数据一致。
方案总览
| 方案 | 一致性 | 性能 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| 2PC | 强一致 | 低 | 低 | 传统数据库跨库 |
| 3PC | 强一致 | 低 | 中 | 理论模型(少用) |
| TCC | 最终一致 | 高 | 高 | 高性能金融场景 |
| Saga | 最终一致 | 高 | 中 | 长事务、微服务 |
| 本地消息表 | 最终一致 | 高 | 低 | 异步场景 |
| MQ 事务消息 | 最终一致 | 高 | 低 | 异步解耦 |
| Seata AT | 最终一致 | 中 | 低 | 微服务通用 |
2PC(两阶段提交)
缺点:
- 同步阻塞:所有参与者在 prepare 后需等待 commit/rollback,持有资源锁
- 单点故障:协调者宕机,参与者会一直阻塞
- 数据不一致:协调者发出部分 commit 后宕机,部分参与者已提交、部分未提交
TCC(Try-Confirm-Cancel)
TCC 将事务拆为三个阶段,每个阶段都是本地事务:
TCC 接口示例
public interface InventoryTccService {
// Try:冻结库存(stock-10, frozen+10)
@TwoPhaseBusinessAction(name = "inventoryTry",
commitMethod = "confirm", rollbackMethod = "cancel")
boolean tryDeduct(BusinessActionContext ctx, String productId, int count);
// Confirm:真正扣减(frozen-10)
boolean confirm(BusinessActionContext ctx);
// Cancel:释放冻结(frozen-10, stock+10)
boolean cancel(BusinessActionContext ctx);
}
TCC 注意事项
- 空回滚:Try 未执行但收到 Cancel(网络超时场景),Cancel 需判断是否有 Try 记录
- 幂等:Confirm 和 Cancel 可能被重复调用,必须做幂等处理
- 悬挂:Cancel 先于 Try 到达,需防止 Try 在 Cancel 之后执行
Saga 模式
将长事务拆分为多个本地事务,每个本地事务有对应的 补偿操作。如果某个事务失败,逐步执行之前事务的补偿操作。
Saga 的两种实现:
| 模式 | 说明 | 优点 | 缺点 |
|---|---|---|---|
| 编排(Choreography) | 各服务通过事件驱动,无中心协调 | 简单、松耦合 | 流程分散、难调试 |
| 协调(Orchestration) | 中心协调器控制事务流程 | 流程清晰、易维护 | 协调器有单点风险 |
本地消息表
Seata 框架
Seata 是阿里开源的分布式事务框架,支持 4 种模式:
| 模式 | 说明 | 侵入性 | 性能 |
|---|---|---|---|
| AT | 自动补偿(类似 2PC,基于 undo log) | 低 | 中 |
| TCC | Try-Confirm-Cancel | 高 | 高 |
| Saga | 长事务补偿 | 中 | 高 |
| XA | 数据库 XA 协议 | 低 | 低 |
Seata AT 模式(使用最简单)
// 只需加 @GlobalTransactional 注解
@GlobalTransactional(timeoutMills = 60000, name = "create-order")
public void createOrder(OrderDTO dto) {
// 1. 创建订单(本地事务)
orderService.create(dto);
// 2. 远程调用:扣减库存
inventoryClient.deduct(dto.getProductId(), dto.getCount());
// 3. 远程调用:扣减余额
accountClient.deduct(dto.getUserId(), dto.getAmount());
// 任意步骤失败,Seata 自动回滚所有服务
}
常见面试问题
Q1: 2PC 有什么缺点?
答案:
- 同步阻塞:prepare 后所有参与者等待 commit,长时间持有数据库锁,性能差
- 单点故障:协调者宕机后参与者一直阻塞等待
- 数据不一致:commit 阶段协调者部分发送成功后宕机,导致部分参与者已提交、部分未提交
3PC 引入了超时机制和 preCommit 阶段,缓解了阻塞问题,但仍无法完全解决数据不一致。
Q2: TCC 和 Saga 的区别?
答案:
| 对比 | TCC | Saga |
|---|---|---|
| 资源锁定 | Try 阶段预留资源 | 无资源预留 |
| 一致性 | 较强(资源冻结) | 较弱(无隔离) |
| 开发成本 | 高(每个服务写3个接口) | 中(正向+补偿) |
| 适用场景 | 高一致性要求的金融场景 | 长事务、流程型业务 |
| 回滚方式 | Cancel 释放预留资源 | 补偿操作逆向执行 |
Q3: 项目中怎么选择分布式事务方案?
答案:
- 强一致 + 短事务:Seata AT 或 2PC/XA
- 强一致 + 高性能:TCC
- 最终一致 + 异步:本地消息表 或 MQ 事务消息
- 最终一致 + 长事务:Saga
- 最终一致 + 简单:先写后补偿 + 定时对账
大多数互联网场景用 最终一致性 方案(MQ 事务消息 / 本地消息表)就够了,性能最好,复杂度最低。
Q4: Seata AT 模式的原理是什么?
答案:
AT 模式基于 undo log 自动回滚:
- 一阶段:拦截 SQL,在执行前记录 before image(数据快照),执行后记录 after image,生成 undo log。本地事务直接提交(释放锁)
- 二阶段 Commit:异步删除 undo log
- 二阶段 Rollback:根据 undo log 中的 before image 生成反向 SQL,执行回滚
优点是对业务零侵入(只加注解),缺点是有全局锁(TC 管理),并发性能不如 TCC。