幂等、重试与故障恢复
可靠执行需要分别处理任务没有送到、动作没有完成、动作已经完成但确认丢失。后两种情况对调度器可能表现为同一种超时,直接重试会产生重复副作用。
重复来自哪里
| 故障位置 | 恢复行为 | 业务风险 |
|---|---|---|
| 发布成功后,未保存发布结果 | 投递器再次发布 | 同一事件重复到达 |
| 业务事务提交后,未确认消息 | 消息重新投递 | 同一业务动作重复处理 |
| Worker 暂停超过租约 | 其他 Worker 重新领取 | 新旧尝试可能同时执行 |
| 外部接口已成功,响应丢失 | 调用方超时重试 | 重复发送、扣款或创建资源 |
RabbitMQ 的消费确认文档说明了连接中断等情况下未确认消息的重新入队。可恢复的处理链路需要接受重复投递的可能,并在业务侧建立约束。
“至少一次”描述的是一定故障与存储保证下的投递或执行语义,不承诺业务动作最终必然成功。永久参数错误、耗尽重试、超出时效和不可用下游,都需要明确的终止与处置方式。
同一数据库中的幂等
以事件触发库存变更为例,去重记录与业务更新可以一起提交:
BEGIN
尝试插入 processed_events(event_id),由唯一约束判重
如果已经存在:结束本次重复处理
执行业务状态更新
COMMIT
提交后再确认消息业务失败时去重记录也必须回滚。先单独提交“已经处理”,再执行业务,会让崩溃后的重试错误跳过尚未完成的动作。
事件 ID 只能去掉相同事件的重复。如果多个事件会要求同一动作,还需要业务键,例如 order_id + notification_type + version。键的保留期限要覆盖消息重放与历史修复窗口。
外部副作用的边界
本地插入唯一通知记录之后再调用供应商,仍有两个窗口:
| 顺序 | 崩溃窗口 | 后果 |
|---|---|---|
| 先记“已发送”,再发送 | 记完后尚未发送 | 可能漏发 |
| 先发送,再记“已发送” | 已发送但尚未记录 | 重试可能重复发送 |
因此,本地唯一约束只能约束本地记录,不能单独保证外部动作恰好发生一次。
如果下游支持幂等请求,每次尝试应携带相同的业务幂等键,并核对下游的作用范围、保存期限和参数一致性约束。Stripe 的幂等请求接口是一个具体例子,其规则不代表所有供应商。
如果下游提供结果查询,超时后可以先查询原请求;查询也可能暂时不确定,需要继续核对。如果下游既不支持幂等也不能查询,就必须根据业务明确取舍重复与遗漏,或转人工处置,不能宣称端到端 Exactly-once。
错误分类与重试预算
| 错误 | 可以采取的处理 | 需要确认的条件 |
|---|---|---|
| 网络中断、响应超时 | 查询结果或重试 | 动作是否可能已经提交 |
| 临时限流、服务过载 | 延迟重试、降低并发 | 下游恢复窗口和重试预算 |
| 参数、格式或版本错误 | 停止自动重试并诊断 | 数据或程序是否需要修复 |
| 凭据或权限错误 | 告警,按恢复流程处理 | 是否存在可恢复的凭据刷新 |
| 业务条件失效 | 记录 skipped | 不要计作基础设施失败 |
指数退避可以用下面的示例表达。这里的第 1 次重试发生在首次失败之后,参数只是设计样例:
base = 1 秒
cap = 5 分钟
第 k 次重试的等待时间 = random(0, min(cap, base × 2^(k - 1)))随机抖动把重试时间分散开;它不增加下游容量。AWS 的超时、重试与退避说明分析了重试放大和同步重试的风险。
任务系统、HTTP 客户端和 SDK 都可能重试。配置时应计算总调用次数,并限制最大尝试次数、总处理时长和全局重试吞吐。可重新入队的任务,等待期间不必一直占用 Worker。
三类超时
- 调用超时:本次等待外部接口多久,包括需要覆盖的连接与请求阶段。
- 执行租约:本次尝试多久不续租就允许回收。
- 业务截止时间:超过什么时间,继续执行已没有价值。
三者需要协调。调用超时不意味着远端已经取消;租约过期不意味着旧 Worker 已停止;超过业务截止时间则应重新判断是否跳过,而非继续机械重试。
故障恢复与人工重放
租约回收只重新开放领取资格,仍要保留尝试上限。失去租约的 Worker 不应继续确认任务;涉及外部调用时,还需要下游幂等保护。
进入 dead 后,应保存错误类别、业务键、原始参数版本和尝试记录。人工重放前检查业务当前状态和已产生的副作用,修复根因后限速恢复。
同一业务动作重放时保留幂等键;如果确实需要发起新的业务动作,应建立新版本并记录原因。单纯更换随机 ID 绕过去重,会使修复过程难以审计。
参考资料
- RabbitMQ:Consumer Acknowledgements and Publisher Confirms
- Stripe:Idempotent requests
- Amazon Builders' Library:Timeouts, retries, and backoff with jitter