同一条任务失败了很多次,下一步仍然是重试。
如果授权已经撤销、请求参数一直不合法,延长队列只能重复相同的失败。另一个容易被忽略的情况是请求超时:客户端没有拿到响应,远端却可能已经完成写入。再次发送可能产生第二笔记录。
设计重试策略前,需要保留失败的原因,并判断重复执行会带来什么后果。
不要把不同结果都压成 null
外部调用只返回 true/false 或 value/null,会让下游丢失判断依据。参数不合法、凭据不可用、网络暂时中断都变成同一个失败值,消费者只能套用统一的重试次数。
可以在调用适配层把结果转换成下面几种语义。这是一种应用层设计,具体错误码仍应按服务提供方的文档映射。
| 结果 | 示例 | 后续处理 |
|---|---|---|
| 正常跳过 | 输入不满足业务执行条件 | 保存跳过原因,不计为调用失败 |
| 持续性阻塞 | 参数需要修正,或授权需要重新取得 | 停止自动重试,等待输入或配置变化 |
| 瞬时失败且重复执行可控 | 短暂不可用、部分限流场景 | 在次数与时间预算内安排重试 |
| 结果待核对 | 写请求超时,无法确认远端是否完成 | 先查远端状态,再决定是否重发 |
这里不能只按 HTTP 状态码分组。例如,认证失败可能需要先刷新凭据;刷新后仍然缺少权限,就应进入待修复状态。限流是否能稍后恢复、应等待多久,也取决于接口契约和返回信息。
请求超时不等于远端没有执行
假设一次"创建记录"请求已经到达远端,但返回响应的连接中断了。客户端只看到超时,无法据此断定创建失败。
RFC 9110 的幂等性说明指出,非幂等请求不能仅因为通信失败就自动重试,需要能够确认该请求实际上可幂等执行,或确认原请求没有应用。
对存在副作用的接口,可以考虑两条路径:
- 服务端支持幂等机制时,按接口要求复用同一业务操作的幂等键,并核对有效期、作用范围和重复请求语义。
- 无法确认重试安全时,使用业务标识查询远端结果;查不到也要考虑接口的可见性延迟,不能立刻当成未执行。
在客户端随手增加一个 requestId,不会让服务端自动具备幂等能力。关键是对方是否识别这个标识,以及重复请求会如何处理。
把重试资格写成一个小函数
下面的 JavaScript 示例只负责选择下一步,不实现队列、网络请求和等待逻辑。输入已经由适配层分类,failedAttempts 表示这次失败后累计执行过多少次;示例最多允许三次执行。
javascript
const MAX_ATTEMPTS = 3;
function nextAction(result) {
if (result.kind === "not_applicable") return "skip";
if (result.kind === "permanent") return "block";
if (result.kind !== "transient") return "review";
if (!Number.isInteger(result.failedAttempts) ||
result.failedAttempts < 1 ||
typeof result.mayHaveCommitted !== "boolean" ||
typeof result.retrySafe !== "boolean") {
return "review";
}
if (result.mayHaveCommitted && !result.retrySafe) {
return "reconcile";
}
return result.failedAttempts < MAX_ATTEMPTS
? "retry"
: "block";
}
retrySafe 需要有明确依据,例如这是没有写入副作用的读取操作,或服务端已经按契约提供幂等保障。不能让模型根据一段报错文本随意把它设成 true。
一个已经失败两次、远端可能完成写入、又没有幂等保障的任务,会得到 reconcile。即使次数还有余额,也不会直接重发。
达到次数上限之后仍可能需要核对远端结果。因此代码先判断"结果是否不确定",再判断能否继续尝试。block 与 reconcile 都不会直接触发新请求,但需要不同的后续处理。
获得重试资格后再计算等待时间
重试资格与调度时间是两件事。确定可以重试后,还需要限制请求总量。
AWS 的超时、重试与抖动退避说明讨论了重试放大后端压力、分层重试叠加,以及通过退避和随机抖动分散请求的问题。这里可以采用以下设计:
- 只在一个明确的调用层负责重试,避免 SDK、服务和队列各自再试一轮。
- 同时限制执行次数、单次超时和任务总截止时间。
- 使用带上限的退避,并通过随机抖动分散同时失败的请求。
收到 Retry-After 时,按服务端给出的等待建议安排后续请求。它可能是秒数,也可能是 HTTP 日期,不能把它固定当成数字解析。MDN 的 Retry-After 说明
如果等待时间已经超出任务总预算,就结束或延后该任务,而不是为了赶截止时间缩短服务端要求的间隔。上面的决策函数并没有实现这些调度能力,接入真实队列时需要另外补齐。
验证时检查容易被误判的分支
本文示例进行了十组本地断言,覆盖正常跳过、持续性阻塞、未知错误、安全重试、结果待核对、达到次数上限和不完整输入,均通过。
有几项检查尤其值得保留:
| 输入条件 | 预期动作 |
|---|---|
| 瞬时失败,确认没有写入,第一次执行失败 | retry |
| 可能已经写入,且没有重试安全保障 | reconcile |
| 可能已经写入,但具备明确的幂等保障,次数有余额 | retry |
| 明确需要修复配置 | block |
| 符合正常跳过条件 | skip |
| 错误类型未知或关键字段缺失 | review |
这些断言验证的是决策函数,不代表第三方接口、队列恢复或真实业务写入已经验收。上线前还应模拟响应丢失、幂等键过期、重试预算耗尽,以及多个任务同时被限流的情形。
排查反复失败的任务时,可以先检查错误信息是否被压扁,再检查重复执行是否安全,最后才调整重试次数和等待间隔。这样每次重试都有依据,无法自愈的问题也能及时转入可处理的状态。