Skip to content

幂等、重试与故障恢复 ​

可靠执行需要分别处理任务没有送到、动作没有完成、动作已经完成但确认丢失。后两种情况对调度器可能表现为同一种超时,直接重试会产生重复副作用。

重复来自哪里 ​

故障位置恢复行为业务风险
发布成功后,未保存发布结果投递器再次发布同一事件重复到达
业务事务提交后,未确认消息消息重新投递同一业务动作重复处理
Worker 暂停超过租约其他 Worker 重新领取新旧尝试可能同时执行
外部接口已成功,响应丢失调用方超时重试重复发送、扣款或创建资源

RabbitMQ 的消费确认文档说明了连接中断等情况下未确认消息的重新入队。可恢复的处理链路需要接受重复投递的可能,并在业务侧建立约束。

“至少一次”描述的是一定故障与存储保证下的投递或执行语义,不承诺业务动作最终必然成功。永久参数错误、耗尽重试、超出时效和不可用下游,都需要明确的终止与处置方式。

同一数据库中的幂等 ​

以事件触发库存变更为例,去重记录与业务更新可以一起提交:

text
BEGIN
  尝试插入 processed_events(event_id),由唯一约束判重
  如果已经存在:结束本次重复处理
  执行业务状态更新
COMMIT
提交后再确认消息

业务失败时去重记录也必须回滚。先单独提交“已经处理”,再执行业务,会让崩溃后的重试错误跳过尚未完成的动作。

事件 ID 只能去掉相同事件的重复。如果多个事件会要求同一动作,还需要业务键,例如 order_id + notification_type + version。键的保留期限要覆盖消息重放与历史修复窗口。

外部副作用的边界 ​

本地插入唯一通知记录之后再调用供应商,仍有两个窗口:

顺序崩溃窗口后果
先记“已发送”,再发送记完后尚未发送可能漏发
先发送,再记“已发送”已发送但尚未记录重试可能重复发送

因此,本地唯一约束只能约束本地记录,不能单独保证外部动作恰好发生一次。

如果下游支持幂等请求,每次尝试应携带相同的业务幂等键,并核对下游的作用范围、保存期限和参数一致性约束。Stripe 的幂等请求接口是一个具体例子,其规则不代表所有供应商。

如果下游提供结果查询,超时后可以先查询原请求;查询也可能暂时不确定,需要继续核对。如果下游既不支持幂等也不能查询,就必须根据业务明确取舍重复与遗漏,或转人工处置,不能宣称端到端 Exactly-once。

错误分类与重试预算 ​

错误可以采取的处理需要确认的条件
网络中断、响应超时查询结果或重试动作是否可能已经提交
临时限流、服务过载延迟重试、降低并发下游恢复窗口和重试预算
参数、格式或版本错误停止自动重试并诊断数据或程序是否需要修复
凭据或权限错误告警,按恢复流程处理是否存在可恢复的凭据刷新
业务条件失效记录 skipped不要计作基础设施失败

指数退避可以用下面的示例表达。这里的第 1 次重试发生在首次失败之后,参数只是设计样例:

text
base = 1 秒
cap = 5 分钟
第 k 次重试的等待时间 = random(0, min(cap, base × 2^(k - 1)))

随机抖动把重试时间分散开;它不增加下游容量。AWS 的超时、重试与退避说明分析了重试放大和同步重试的风险。

任务系统、HTTP 客户端和 SDK 都可能重试。配置时应计算总调用次数,并限制最大尝试次数、总处理时长和全局重试吞吐。可重新入队的任务,等待期间不必一直占用 Worker。

三类超时 ​

  • 调用超时:本次等待外部接口多久,包括需要覆盖的连接与请求阶段。
  • 执行租约:本次尝试多久不续租就允许回收。
  • 业务截止时间:超过什么时间,继续执行已没有价值。

三者需要协调。调用超时不意味着远端已经取消;租约过期不意味着旧 Worker 已停止;超过业务截止时间则应重新判断是否跳过,而非继续机械重试。

故障恢复与人工重放 ​

租约回收只重新开放领取资格,仍要保留尝试上限。失去租约的 Worker 不应继续确认任务;涉及外部调用时,还需要下游幂等保护。

进入 dead 后,应保存错误类别、业务键、原始参数版本和尝试记录。人工重放前检查业务当前状态和已产生的副作用,修复根因后限速恢复。

同一业务动作重放时保留幂等键;如果确实需要发起新的业务动作,应建立新版本并记录原因。单纯更换随机 ID 绕过去重,会使修复过程难以审计。

参考资料 ​