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