关键在异常分类与状态边界:业务异常不补偿,系统异常必补偿,流程异常禁补偿;补偿前须校验状态、保障幂等、严守状态机跃迁规则,并通过全链路traceid和错误类型透传精准归因。

关键不在“补偿是否触发”,而在“该不该触发”——长事务中补偿被误执行,本质是异常分类模糊、状态边界不清、业务语义未对齐。
明确区分三类异常:业务异常、系统异常、流程异常
长事务(如 Saga)依赖显式状态跃迁,但很多团队把所有 Exception 一律当失败处理,导致补偿无差别启动:
- 业务异常:如“库存不足”“用户余额为负”,属于预期内校验失败,应终止流程但不触发补偿(因为 Try 阶段未真正锁定资源,或已主动释放)
- 系统异常:如 RPC 超时、数据库连接中断、序列化失败,属于非预期中断,Try 阶段可能已部分生效,必须触发补偿
-
流程异常:如 Saga 编排器收到重复请求、状态机跳转非法(比如从
CONFIRMING直接到CANCELLED),需拦截并记录审计日志,禁止进入补偿逻辑
建议在每个 Try 方法出口统一做异常分类包装,例如:
if (e instanceof BusinessException) {
throw new SkipCompensationException("库存不足,无需补偿", e);
} else if (e instanceof TimeoutException || e instanceof IOException) {
throw new RequireCompensationException("远程调用超时,需补偿", e);
}
检查补偿方法的幂等前提与前置校验
补偿错误常因“没确认当前状态就直接执行逆操作”。例如:库存释放接口未查锁记录,直接调用 increaseStock(),结果把本就没扣减成功的库存又加回去。
- 每个补偿方法开头必须先查状态表(如
inventory_lock),确认对应资源确实处于“已锁定且未释放”状态 - 使用唯一业务键(如
orderId + skuId)做幂等控制,避免重复补偿;补偿成功后立即更新状态为CANCELLED或写入幂等记录表 - 禁止在补偿方法中调用可能再次抛出异常的外部服务(如发消息、调第三方 API),这些动作应通过异步事件+重试机制解耦
验证 Saga 状态机是否严格守序
状态跃迁失控是补偿误触发的隐藏推手。例如:Try 成功后本应进入 CONFIRMING,却因网络抖动或日志丢失被误判为失败,直接跳转 CANCELLING。
- 检查状态存储(如 DB 或 Redis)是否开启事务/原子操作,防止状态写入丢失
- 所有状态变更必须带版本号或时间戳,并在更新前校验前序状态(如只允许从
TRYING→CONFIRMING,不允许跨步) - 启用 Saga 执行日志审计表,每步操作记录:
stepName、status、prevStatus、triggerCause(是超时?还是手动干预?)
用 TraceID 对齐全链路异常上下文
补偿被误触发,往往是因为上游服务捕获了异常但没透传真实原因,下游只能按默认策略兜底。
- 确保 Feign/Dubbo/RocketMQ 拦截器中,将原始异常类型、错误码、业务上下文(如
errorCode=INVENTORY_SHORTAGE)注入 MDC 或消息头 - 补偿服务消费消息后,优先解析
errorType字段,而非仅依赖 HTTP 状态码或空异常堆栈 - 在 SkyWalking 或 Jaeger 中,给每个 Saga 步骤打上
span.tag("saga.step", "reserveStock")和span.tag("saga.error.category", "BUSINESS"),便于按类别聚合分析误触发比例










