核心是让异常不堆积、不传播、不掩盖真实瓶颈;优惠券大促失败源于补偿逻辑缺失导致状态失序,有效补偿需在catch内闭环执行、绑定完整上下文、按根因分通道处理、带状态机与幂等标识,并使前端与运营可感知进展。

核心不是补异常,而是让异常不堆积、不传播、不掩盖真实瓶颈。优惠券在大促中出现“扣减失败→反复重试→线程卡死→CPU飙升→用户看到白屏或超时”,表面是技术故障,实则是补偿逻辑缺失导致的业务状态失序。真正有效的补偿器必须扎根于 catch 块内可执行、可追溯、可收敛 的闭环动作,而非依赖外部调度或事后人工干预。
补偿动作必须绑定原始操作上下文
不能只记“券ID失败了”,要记录当时已发生的全部事实:
- 扣减前库存值(如 Redis 中读到的 stock=5)、用户ID、请求时间戳、traceID、本地事务ID
- 已成功写入的中间状态(如已插入 coupon_record 表但未更新 coupon 表)
- 调用下游的响应快照(如用户中心返回 HTTP 200 但 body 为空,或风控返回 code=503)
这些信息必须在 try 块中就采集并透传至 catch,否则补偿时查不到真实状态,容易误判——比如查 DB 发现订单已存在,就跳过恢复库存,结果实际是两次创建了同一订单。
按失败根因分通道执行补偿,拒绝统一重试
同一张券失败,原因不同,处理方式必须隔离:
- 缓存穿透型(Redis 返回 null,DB 有值):在 catch 中触发异步懒加载,仅写回带随机过期时间的缓存,不阻塞主流程;同一 key 的补偿请求自动合并去重
- 版本冲突型(CAS 更新影响行为为 0):立即进入“有序重试通道”,按 couponId 哈希到固定线程池,sleep(10~50ms 随机)后再重试,避免多线程反复碰撞消耗 CPU
- 下游超时型(调用鉴权/风控超时):不重试,立刻落盘 RingBuffer 快照,由后台线程每 200ms 批量捞取、去重、重发;失败则写入 Kafka 持久化,供运营侧人工核验
补偿必须带状态机与幂等标识
直接在 catch 里删订单、加库存,风险极高。应先更新主表状态字段(如 status = 'rollbacking'),再触发补偿链:
- 所有补偿方法命名带 rollback 前缀,返回 boolean,并捕获各自异常(不抛出)
- 补偿顺序严格逆序:先撤通知(如有)、再删订单、最后恢复库存;每步独立 try-catch,失败即记日志告警
- 恢复库存用 “+原始扣减量” 而非 “设为原始值”,避免并发覆盖;订单删除加 where orderId = ? and status = 'created' 条件
前端与运营需感知补偿进展
用户不应只看到“领取失败”,而应获得可预期反馈:
- 对已唤起支付 SDK 的用户,补偿器标记 high-priority,前端轮询 /callback 接口返回 “正在处理,请稍候”,3 秒内未完成则提示“已锁定资格,10 分钟内到账”
- 对仅浏览券列表的用户,直接返回“暂无库存”,不进任何补偿队列,释放资源
- 运营后台实时看板展示:各通道失败率、平均补偿耗时、待人工介入数、近 1 小时自愈率(补偿成功 / 总失败)

















