299 lines
7.7 KiB
Markdown
299 lines
7.7 KiB
Markdown
# 分布式事务
|
||
|
||
## 问题
|
||
|
||
**背景**:在微服务架构中,我们经常遇到跨服务的数据一致性问题。
|
||
|
||
**问题**:
|
||
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 理论在实际场景中的权衡
|
||
- 能根据业务特点选择合适的一致性级别
|
||
- 有监控和告警体系,能快速定位分布式事务问题
|
||
|
||
**架构能力**:
|
||
- 能设计支持多种分布式事务模式的统一框架
|
||
- 考虑降级和熔断策略
|
||
- 有混沌工程实践(注入故障测试系统恢复能力)
|