分布式事务
问题
什么是分布式事务?2PC、TCC、Saga 各自是什么原理?如何在实际项目中选择合适的分布式事务方案?
答案
为什么需要分布式事务
在微服务或分库分表架构中,一个业务操作可能涉及多个数据库或多个服务,本地事务无法保证跨库/跨服务的数据一致性。
核心问题:订单创建成功,但库存扣减失败,数据不一致。
分布式事务方案概览
| 方案 | 一致性 | 性能 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| 2PC | 强一致性 | 低 | 低 | 单应用跨库 |
| TCC | 最终一致性 | 较高 | 高 | 金融/电商核心链路 |
| Saga | 最终一致性 | 高 | 中 | 长事务、跨服务 |
| 本地消息表 | 最终一致性 | 高 | 中 | 异步解耦 |
| 事务消息 | 最终一致性 | 高 | 低 | RocketMQ 生态 |
2PC(两阶段提交)
2PC(Two-Phase Commit)是最经典的分布式事务协议,由一个 协调者(Coordinator) 和多个 参与者(Participant) 组成。
如果有参与者 Prepare 失败:
2PC 的问题:
| 问题 | 说明 |
|---|---|
| 同步阻塞 | Prepare 后参与者锁定资源,等待协调者决策 |
| 单点故障 | 协调者宕机,参与者锁定资源无法释放 |
| 数据不一致 | Commit 发送给部分参与者后协调者宕机 |
| 并发低 | 整个过程锁定资源 |
MySQL 中的 2PC(XA 事务):
-- XA 事务(跨库事务)
XA START 'txn1';
INSERT INTO db1.orders VALUES (...);
XA END 'txn1';
XA PREPARE 'txn1';
-- 另一个连接
XA START 'txn2';
UPDATE db2.stock SET quantity = quantity - 1;
XA END 'txn2';
XA PREPARE 'txn2';
-- 两个都 Prepare 成功后
XA COMMIT 'txn1';
XA COMMIT 'txn2';
TCC(Try-Confirm-Cancel)
TCC 将业务逻辑分为三个阶段:
以转账为例:
| 阶段 | A 账户(转出方) | B 账户(转入方) |
|---|---|---|
| Try | 检查余额 ≥ 100,冻结 100 | 无操作(或预增) |
| Confirm | 扣减冻结金额 100 | 增加 100 |
| Cancel | 解冻 100 | 回滚预增 |
// TCC 接口设计
public interface AccountTccService {
// Try:冻结金额
@TwoPhaseBusinessAction(name = "deduct",
commitMethod = "confirm", rollbackMethod = "cancel")
boolean tryDeduct(BusinessActionContext ctx,
String accountId, BigDecimal amount);
// Confirm:扣减冻结金额
boolean confirm(BusinessActionContext ctx);
// Cancel:解冻金额
boolean cancel(BusinessActionContext ctx);
}
TCC 需要解决的问题:
| 问题 | 解决方案 |
|---|---|
| 空回滚 | Cancel 被调用但 Try 未执行 → Cancel 需判断 Try 是否执行过 |
| 幂等 | Confirm/Cancel 可能被重复调用 → 需要幂等设计 |
| 悬挂 | Cancel 先于 Try 到达 → Try 需判断是否已 Cancel |
Saga 模式
Saga 将长事务拆分为一系列 本地短事务,每个本地事务有对应的 补偿操作。
两种编排方式:
| 方式 | 说明 | 优势 | 劣势 |
|---|---|---|---|
| 编排式(Orchestration) | 中央协调器控制流程 | 流程清晰、易于管理 | 协调器是单点 |
| 协同式(Choreography) | 各服务通过事件驱动 | 无中心、松耦合 | 流程分散、难追踪 |
本地消息表
利用数据库的本地事务,将业务操作和消息写入放在同一个事务中。
优势:不依赖分布式事务框架,实现简单
劣势:需要维护消息表,定时扫描有延迟
事务消息(RocketMQ)
RocketMQ 原生支持事务消息,原理类似本地消息表但更优雅。
方案对比与选型
| 维度 | 2PC | TCC | Saga | 本地消息表 | 事务消息 |
|---|---|---|---|---|---|
| 一致性 | 强一致 | 最终一致 | 最终一致 | 最终一致 | 最终一致 |
| 性能 | 低 | 较高 | 高 | 高 | 高 |
| 业务侵入 | 无 | 高(3个接口) | 中(补偿接口) | 低 | 低 |
| 隔离性 | 强 | 弱(需业务保证) | 无 | 无 | 无 |
| 复杂度 | 低 | 高 | 中 | 中 | 低 |
| 框架 | XA | Seata TCC | Seata Saga | 自行实现 | RocketMQ |
Seata 分布式事务框架
Seata 是阿里开源的分布式事务框架,支持 AT、TCC、Saga、XA 四种模式。
AT 模式(最常用,自动补偿):
- 一阶段:解析 SQL,自动记录数据快照(before image),执行业务 SQL
- 二阶段-提交:删除快照(异步,很快)
- 二阶段-回滚:用快照恢复原数据
优势:对业务无侵入,像本地事务一样使用
劣势:需要全局锁,性能有一定损耗
常见面试问题
Q1: 2PC 有什么问题?
答案:
- 同步阻塞:所有参与者在 Prepare 后锁定资源等待,降低并发
- 单点故障:协调者宕机后参与者资源无法释放
- 数据不一致:协调者发送 Commit 给部分参与者后宕机,已收到的提交了,未收到的还在等
- 性能差:两轮网络通信 + 资源锁定时间长
Q2: TCC 如何处理空回滚和幂等?
答案:
空回滚:Cancel 被调用但 Try 还未执行(如 Try 超时,协调者先发了 Cancel)。
- 解决:在 Cancel 中检查业务记录,如果 Try 没执行过,直接返回成功
幂等:网络重试导致 Confirm/Cancel 被重复调用。
- 解决:记录事务执行状态(幂等表),重复调用直接返回
悬挂:Cancel 先到达并执行完毕,之后 Try 才到达。
- 解决:Try 执行前检查是否已经 Cancel,如果已 Cancel 则不执行
Q3: Saga 和 TCC 的区别?
答案:
| 对比 | TCC | Saga |
|---|---|---|
| 资源锁定 | Try 阶段预留 | 不预留,直接执行 |
| 隔离性 | 有(冻结资源期间其他事务看不到) | 无(直接修改,中间状态可见) |
| 补偿方式 | Cancel 回滚预留 | 反向操作补偿 |
| 实时性 | 资源锁定期间不可用 | 资源始终可用 |
| 适用场景 | 金融(需要隔离性) | 长流程(需要高可用) |
Q4: 实际项目中如何选择分布式事务方案?
答案:
- 能用本地事务就不用分布式事务:通过合理的服务设计减少跨服务事务
- 数据一致性要求高(金融):TCC 或 2PC
- 长流程编排:Saga
- 异步解耦:本地消息表或事务消息(RocketMQ)
- Java 生态:Seata AT 模式(对业务无侵入)
实际中,大部分互联网业务用 最终一致性 就够了(本地消息表 + 补偿),只有金融核心链路才需要 TCC。
Q5: 什么是 BASE 理论?和 ACID 什么关系?
答案:
BASE 是分布式系统对 ACID 的妥协:
- BAsically Available:基本可用(允许部分功能降级)
- Soft state:软状态(允许存在中间状态)
- Eventually consistent:最终一致性(不要求实时一致)
关系:
- ACID → 单数据库的强一致性保证
- BASE → 分布式系统在 CAP 限制下的实用性妥协
- 分布式事务(TCC/Saga/消息)本质上是在实现 BASE 的最终一致性
更多 CAP/BASE 理论详见 Java 系列 CAP 与 BASE。