补偿任务如何设计才可靠?
简化版
可靠补偿任务必须有权威状态、稳定业务键、幂等执行、可恢复进度和明确终态。 任务从异常事件、状态扫描或对账差异产生,经过抢占、条件检查、执行、结果确认和重试,不能只是无限循环定时器。 重试有退避与上限,永久失败进入人工队列,执行前重读当前状态以防迟到补偿覆盖新结果。
详细版
任务记录 taskId、businessKey、expectedVersion、动作、状态、attempt、nextRunAt、leaseOwner 和 lastError。 Worker 用租约或条件更新抢任务,外部动作带幂等键,完成时 CAS 更新状态。 若外部成功后进程崩溃,重试必须查询结果或幂等重复。 系统区分瞬时、限流、业务拒绝与永久错误,监控积压年龄、成功率、死信和不一致持续时间。
完整版教学
一、补偿不是简单重试
重试再次执行原动作,补偿也可能执行逆向或替代动作。 订单已创建但积分未发时可补发;支付已撤销时则可能需要取消积分。 任务必须围绕当前业务不变量决策。
补偿的目标是恢复正确业务状态,而不是机械回放旧请求。
二、任务数据模型
| 字段 | 作用 |
|---|---|
| businessKey | 唯一业务对象 |
| expectedVersion | 防迟到覆盖 |
| action/payload | 补偿意图 |
| status/attempt | 状态与次数 |
| nextRunAt | 退避调度 |
| lease | 防并发领取 |
payload 保存必要引用,不复制可从权威源读取的敏感大对象。
三、状态机与租约
PENDING -> RUNNING -> SUCCEEDED
|-> RETRY_WAIT -> RUNNING
|-> MANUAL_REVIEW
|-> CANCELLED
RUNNING 使用有限租约,Worker 崩溃后可重新领取。 终态保留审计,不能成功后立即删除。
四、幂等执行
taskId 或业务操作号传给下游作为幂等键。 下游在同一事务内记录去重键和业务结果。 响应丢失时查询原结果或重复同一幂等请求。 本地标记“发送过”不能证明下游成功。
五、执行前校验
任务创建时订单版本 7,执行时可能已到版本 10。 补偿应读取权威状态并检查 expectedVersion 和合法前置条件。 状态已自然恢复则标记 RESOLVED。 状态走向相反则取消或转人工,不能回写旧状态。
六、重试与限流
瞬时故障按 5 秒、30 秒、2 分钟等节奏退避并加抖动。 参数非法、账户冻结等永久错误不应无限重试。 下游限流时遵循 Retry-After,避免补偿流量加重故障。 到达次数或时间上限后进入人工队列。
七、扫描与观测
按 nextRunAt 索引分页领取,使用游标而非深分页。 分片归属要稳定,扩容时仍靠租约与幂等保护。 监控 oldest_pending_age、到期积压、attempt 分布和人工积压。 独立对账负责发现任务创建链路本身漏掉的异常。
八、常见误区与追问
- 误区:失败就无限重试最可靠。 永久错误会形成重试风暴。
- 误区:单实例任务不需要幂等。 崩溃和响应丢失仍会重复。
- 误区:补偿按创建时数据执行即可。 当前状态可能已变化。
- 误区:任务成功后立刻删除。 会失去审计与结果查询。
- 追问:外部成功、本地未记成功怎么办? 用幂等键查询或重复。
- 追问:怎样避免多 Worker 重复? 租约领取加下游幂等。
- 追问:任务漏创建怎么办? 权威状态扫描和独立对账再生任务。
九、加强记忆
- 补偿围绕业务不变量。
- 任务有状态机、租约、进度和审计。
- 下游动作使用稳定业务键幂等。
- 执行前重读状态,防迟到覆盖。
- 重试有退避和人工终点,对账负责补漏。