2. Saga 执行流程与补偿机制:正向事务与补偿事务的执行顺序、补偿操作的幂等性要求
2.1 Saga 事务模型的核心执行顺序
Saga 是一种长事务解决方案,将一个全局事务拆分为多个本地子事务(正向事务),并为每个子事务注册一个对应的补偿事务。其核心执行顺序遵循 “正向执行,逆向补偿” 的原则。
关键原则: 正向事务按编排顺序依次执行;一旦任意一个正向事务失败,Saga 引擎立即按 反向顺序 调用已成功执行的正向事务对应的补偿事务,以回滚整个业务流程。
以下是一个典型的 Saga 执行流程(以订单创建为例):
- 步骤 1(正向): 订单服务创建订单(状态:待支付)。
- 步骤 2(正向): 支付服务扣减用户余额。
- 步骤 3(正向): 库存服务扣减商品库存。
- 步骤 4(正向): 物流服务创建发货单。
- 步骤 5(成功): 所有正向事务完成,全局事务结束。
若步骤 3(库存扣减)失败,则执行补偿流程:
- 补偿步骤 1: 调用支付服务的补偿事务(退还用户余额)。
- 补偿步骤 2: 调用订单服务的补偿事务(将订单状态置为“已取消”)。
- 补偿结束: 全局事务回滚完成。
2.2 补偿操作的幂等性要求
在分布式系统中,网络超时、节点故障、重试机制等因素可能导致补偿事务被多次调用。因此,补偿操作必须实现幂等性,即多次执行与一次执行产生相同的业务结果,不会造成数据不一致或重复扣减。
| 幂等性等级 | 实现方式 | 适用场景 | 示例 |
|---|---|---|---|
| 天然幂等 | 操作本身多次执行结果相同 | 状态更新、删除操作 | 将订单状态置为“已取消” |
| 业务键去重 | 使用全局唯一业务 ID 进行防重 | 资金操作、库存变更 | 退款时检查 refund_id 是否已处理 |
| 版本号控制 | 乐观锁 + 版本号校验 | 并发较高的资源更新 | UPDATE ... WHERE version = old_version |
| 状态机校验 | 仅允许特定状态流转 | 订单、工单等有状态实体 | 只有“待支付”状态才允许取消 |
⚠️ 常见陷阱: 补偿事务中直接执行“余额增加”操作,若未做幂等处理,多次补偿会导致用户余额被重复增加,造成资金损失。必须使用事务 ID + 去重表或状态机来保证幂等。
2.3 正向事务与补偿事务的配对关系
每个正向事务必须对应一个明确的补偿事务,且补偿逻辑必须能够完全撤销正向事务的副作用。以下是常见的配对模式:
- 创建 ↔ 删除: 创建订单 → 取消订单(软删除或状态置为无效)。
- 扣减 ↔ 增加: 扣减库存 → 恢复库存;扣减余额 → 退还余额。
- 预留 ↔ 释放: 预留资源 → 释放预留资源(常用于 TCC 模式,但 Saga 中也可使用)。
- 发送消息 ↔ 发送撤销消息: 发送通知 → 发送取消通知(或标记为已撤销)。
2.4 幂等性实现示例(代码片段)
以下是一个基于 业务键去重 的补偿幂等实现示例:
// 补偿事务:退还用户余额
public boolean compensateRefund(CompensationRequest request) {
String refundId = request.getRefundId(); // 全局唯一业务键
// 1. 检查去重表,防止重复处理
if (idempotentChecker.isProcessed(refundId)) {
log.info("补偿事务已处理,跳过重复执行,refundId={}", refundId);
return true; // 返回成功,避免 Saga 引擎继续重试
}
// 2. 执行实际退款逻辑
boolean success = accountService.increaseBalance(request.getUserId(), request.getAmount());
if (success) {
// 3. 标记该业务键为已处理
idempotentChecker.markProcessed(refundId);
}
return success;
}
💡 最佳实践: 补偿事务的幂等性检查应优先于业务逻辑执行。使用数据库唯一索引或 Redis 原子操作来保证去重记录的可靠性。同时,补偿事务的返回值应明确告知 Saga 引擎“已处理”或“无需重试”,避免无谓的超时重试。
2.5 执行顺序与幂等性对故障排查的影响
在故障排查场景中,理解 Saga 的执行顺序和幂等性要求有助于快速定位问题根因:
- 超时场景: 若补偿事务因数据库连接池耗尽而超时,Saga 引擎会触发重试。如果补偿操作未实现幂等,重试可能导致数据重复修改。
- 连接池水位过高: 当连接池被长时间占用(如慢 SQL 或锁等待),补偿事务可能排队超时。此时需要检查补偿事务的 SQL 执行计划,并确保幂等性检查的查询效率足够高。
- 死锁与重试风暴: 多个 Saga 实例同时执行补偿,若幂等性检查未加锁或未使用原子操作,可能引发死锁或数据竞争,进一步推高连接池水位。
因此,在设计 Saga 补偿机制时,必须将 幂等性 作为第一优先级,并结合连接池监控和超时配置进行压测验证。