7.7 KiB
7.7 KiB
分布式事务
问题
背景:在微服务架构中,我们经常遇到跨服务的数据一致性问题。
问题:
- 请描述分布式事务的常见解决方案(至少 3 种)
- 它们的优缺点和适用场景是什么?
- 你在实际项目中是如何选择的?有没有遇到过什么坑?
标准答案
1. 常见解决方案
方案一:2PC (Two-Phase Commit,两阶段提交)
原理:
- 准备阶段:协调者询问所有参与者是否可以提交
- 提交阶段:如果所有参与者都回复"可以",则发送提交指令;否则发送回滚指令
优点:
- 强一致性
- 原理简单,易于理解
缺点:
- 同步阻塞:所有参与者在事务提交前都处于阻塞状态
- 单点故障:协调者故障会导致参与者一直阻塞
- 数据不一致:在第二阶段,部分节点收到提交指令,部分未收到
适用场景:
- 传统关系型数据库(XA 协议)
- 对一致性要求极高,可接受性能损耗的场景
实际应用:
- MySQL XA 事务
- Java JTA (Java Transaction API)
方案二:3PC (Three-Phase Commit,三阶段提交)
原理: 在 2PC 基础上增加 CanCommit 阶段:
- CanCommit:协调者询问参与者是否可以执行
- PreCommit:参与者预执行并回复
- DoCommit:正式提交
优点:
- 相比 2PC 减少了阻塞时间
- 引入超时机制,参与者可以自动决策
缺点:
- 仍然存在数据不一致风险
- 协议更复杂,实现成本高
- 性能提升有限
适用场景:
- 很少在实际生产中使用,更多是理论意义
方案三:TCC (Try-Confirm-Cancel,补偿事务)
原理:
- Try 阶段:尝试执行业务,完成资源的检查和预留
- Confirm 阶段:确认执行业务,使用 Try 阶段预留的资源
- Cancel 阶段:取消执行业务,释放 Try 阶段预留的资源
代码示例:
// Try 阶段
public boolean try() {
// 检查账户余额
// 冻结相应金额(预留资源)
// return true/false
}
// Confirm 阶段
public boolean confirm() {
// 扣除冻结金额
// 真正完成转账
}
// Cancel 阶段
public boolean cancel() {
// 释放冻结金额
// 恢复原始状态
}
优点:
- 最终一致性
- 性能较好,相比 2PC 没有长时间锁资源
- 业务可控性强
缺点:
- 代码侵入性强:每个业务都需要写三个接口
- 开发成本高:需要考虑各种异常情况
- 容易遗漏:Cancel 接口如果实现不完整会导致资源泄露
适用场景:
- 对性能有一定要求
- 业务逻辑清晰,可以拆分成 Try/Confirm/Cancel
- 高并发场景
实际应用:
- 阿里巴巴 Seata 的 TCC 模式
- 支付系统、订单系统
方案四:本地消息表(异步确保)
原理:
- 上游服务在同一本地事务中:
- 完成业务操作
- 存储一条消息到本地消息表(状态为"待发送")
- 定时任务扫描消息表,发送消息到 MQ
- 下游服务消费 MQ,执行业务逻辑
- 下游服务成功后通知上游更新消息状态
优点:
- 实现简单
- 可靠性高(消息持久化)
- 支持重试
缺点:
- 需要维护本地消息表
- 定时任务有延迟
- 需要处理消息重复消费(幂等性)
适用场景:
- 可以接受最终一致性
- 对实时性要求不高
- 高并发场景
实际应用:
- 支付宝到账通知
- 订单创建后的物流通知
方案五:MQ 事务消息(RocketMQ 方案)
原理:
- 发送半消息(Half Message)到 MQ(消息对消费者不可见)
- 执行本地事务
- 提交/回滚消息:
- 本地事务成功 → 提交消息(消息对消费者可见)
- 本地事务失败 → 删除消息
- MQ 提供反查机制:如果长时间未收到确认,主动查询业务方事务状态
优点:
- 解耦性强
- 性能好
- 支持大规模分布式事务
缺点:
- 依赖特定 MQ(如 RocketMQ)
- 需要实现反查接口
- 消息可能有延迟
适用场景:
- 高并发、大规模分布式系统
- 可以接受最终一致性
- 需要解耦上下游服务
实际应用:
- RocketMQ 事务消息
- 双11 大促场景
方案六:Saga 模式
原理: 将长事务拆分为多个本地短事务,每个短事务都有对应的补偿操作:
- 正向操作:T1, T2, T3, ..., Tn
- 补偿操作:Cn, ..., C3, C2, C1(反向补偿)
示例:
预订行程 Saga:
1. 预订航班 (T1)
2. 预订酒店 (T2)
3. 预订租车 (T3)
如果 T2 失败:
1. 取消航班 (C1)
2. 返回失败给用户
优点:
- 适合长事务、业务流程复杂的场景
- 最终一致性
- 可以跨多个服务
缺点:
- 需要为每个操作设计补偿逻辑
- 补偿操作可能失败,需要处理
- 无法保证隔离性(脏读问题)
适用场景:
- 业务流程长、涉及多个服务
- 旅行预订、电商下单
- 微服务编排
实际应用:
- Apache ServiceComb Saga
- Netflix Conductor
2. 方案对比总结
| 方案 | 一致性 | 性能 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| 2PC | 强一致性 | 低(同步阻塞) | 低 | 传统数据库 |
| 3PC | 强一致性 | 中 | 中 | 很少使用 |
| TCC | 最终一致性 | 高 | 高(业务侵入) | 高并发、强业务控制 |
| 本地消息表 | 最终一致性 | 中 | 低 | 高可靠性、可接受延迟 |
| MQ 事务消息 | 最终一致性 | 高 | 中 | 大规模、高并发、解耦 |
| Saga | 最终一致性 | 高 | 高(补偿逻辑) | 长事务、业务编排 |
3. 实际项目选择建议
选择决策树:
是否需要强一致性?
├─ 是 → 2PC(XA 事务)
└─ 否 → 最终一致性
│
├─ 业务可以拆分为 Try/Confirm/Cancel?
│ ├─ 是 → TCC(高并发、强控制)
│ └─ 否 → 继续判断
│
├─ 使用 RocketMQ?
│ ├─ 是 → MQ 事务消息
│ └─ 否 → 继续判断
│
├─ 业务流程长、涉及多服务?
│ ├─ 是 → Saga
│ └─ 否 → 本地消息表
常见坑和注意事项:
-
幂等性问题(所有方案都需要考虑)
- 重复请求导致的重复扣款、重复发货
- 解决:使用唯一业务 ID、Redis 分布式锁
-
空补偿问题(TCC)
// Cancel 被调用时,Try 可能还没执行 public void cancel() { // 需要检查是否有冻结记录 if (没有冻结记录) { return; // 空补偿,直接返回 } // 执行取消逻辑 } -
悬挂问题(TCC)
- Confirm 比 Cancel 先到
- 解决:记录事务状态,拒绝后续操作
-
消息丢失(MQ 方案)
- 网络抖动导致消息丢失
- 解决:ACK 机制 + 重试 + 死信队列
-
资源锁定时间(2PC)
- 长时间锁资源导致性能下降
- 解决:控制事务规模,拆分大事务
4. 阿里 P7 加分项
实际项目经验:
- 设计并实现过千万级用户的分布式事务系统
- 处理过分布式事务的性能瓶颈(如连接池优化、并发度控制)
- 有 TCC/Saga 的踩坑经验和解决方案
深度理解:
- 理解 CAP 理论在实际场景中的权衡
- 能根据业务特点选择合适的一致性级别
- 有监控和告警体系,能快速定位分布式事务问题
架构能力:
- 能设计支持多种分布式事务模式的统一框架
- 考虑降级和熔断策略
- 有混沌工程实践(注入故障测试系统恢复能力)