接口超时后再调几次,读请求可能只是多耗流量;如果是下单、支付或领取优惠券,第一次已经成功就会制造重复业务。
核心结论:只有“错误可能短暂恢复”,并且“重复执行不会改变业务结果”的请求,才适合自动重试。次数只是最后一个参数,幂等性、错误类型、时间预算和取消条件才是前提。
先问三个问题
决定重试前,按顺序回答:
- 这次失败等一会儿可能恢复吗?
- 原请求可能已经被服务端执行了吗?
- 即使执行两次,最终业务效果仍然一样吗?
超时时最难判断:客户端没收到响应,只能证明“结果未知”,不能证明服务端没有执行。
哪些请求通常可以重试
HTTP 规范把 GET、HEAD、OPTIONS、TRACE 定义为安全方法;安全方法以及 PUT、DELETE 也属于幂等方法。但方法名只是起点,最终要看接口契约:
| 请求 | 自动重试判断 |
|---|---|
| 查询商品、配置等 GET | 通常可以,但接口不能夹带领取、计数等业务写入 |
| 把资料 PUT 为同一目标状态 | 通常可以,仍要处理版本冲突 |
| 删除同一资源 | 最终状态仍是不存在,可重试;第二次响应可能变成 404 |
| 创建订单、支付等 POST | 默认不安全,除非服务端提供可靠幂等机制 |
| PATCH | 取决于语义;“设为 10”可能幂等,“增加 10”通常不是 |
幂等说的是最终效果,不是每次状态码和响应体必须相同。
哪些错误值得重试
常见候选包括网络短暂不可达、连接重置,以及 408、429、502、503、504。前提仍是请求可以安全重复,并符合当前 API 契约。
下面这些情况通常不应自动重试:
- 参数或数据格式错误,例如 400、422;
- 权限不足、业务规则拒绝;
- 证书校验失败;
- 用户主动取消;
- 没有幂等保证的写请求;
- 服务端已明确给出不可恢复错误。
401 应进入受控的 Token 刷新流程,限制请求重放次数。409 通常要先获取最新版本并解决冲突,不能原样盲重试。
POST 怎样做到可以重试
创建订单这类操作需要服务端支持幂等键。客户端在用户第一次提交时生成一次业务键,后续网络重试始终携带同一个键和相同请求内容:
final pendingOrder = PendingOrder(
operationId: idGenerator.newId(),
idempotencyKey: idGenerator.newId(),
payload: orderPayload,
);
await pendingOrderStore.save(pendingOrder);
await orderApi.createOrder(
pendingOrder.payload,
idempotencyKey: pendingOrder.idempotencyKey,
);App 重启后也必须恢复原来的键。每次尝试都生成新键,服务端看到的仍是多个新订单;只有用户明确“再下一单”时才创建新键。
服务端要原子保存幂等键、请求摘要和结果,并拒绝“同键不同内容”。客户端本地 Map 无法覆盖多设备、进程重启和响应丢失。
不要固定间隔立即重试
大量客户端固定间隔重试会形成重试风暴。常用策略是指数退避并加入 Jitter(随机抖动):
upper = min(maxDelay, baseDelay * 2^retryIndex)
delay = random(0, upper)同时限制:
- 最大尝试次数;
- 总 Deadline,而不只是单次超时;
- 单个请求的唯一重试层;
- 页面退出、会话变化和用户取消;
- 网络变化和任务是否仍有意义。
HTTP Client、Repository 和任务队列若各重试三次,最坏会放大成 27 次调用。应明确唯一重试所有者。
服务端返回 Retry-After 时应尊重其节奏,但仍受总 Deadline 和取消状态约束。
页面生命周期也要纳入重试
商品查询页面退出后,用户通常已不关心结果,应取消退避等待和后续尝试。取消网络等待不代表服务端停止执行,因此写请求仍要靠幂等键和状态查询兜底。
必须跨页面继续的任务,应交给更长生命周期的 Controller 或任务队列,并持久化操作身份和进度。
怎样验证重试策略
不要用真实弱网碰运气。通过 Fake Client 和可控时钟验证:
- 前两次 503、第三次成功,调用次数和延迟符合策略;
- 400、403 或业务拒绝不会重试;
- 退避期间取消后不再发送请求;
- POST 超时后的所有尝试使用同一幂等键;
Retry-After、最大次数和总 Deadline 能终止流程。
日志至少记录操作 ID、attempt、重试原因、等待时长和最终结果,但不要记录 Token、密码等敏感信息。
最后记住这几点
- 超时表示结果未知,不表示服务端没有执行。
- 自动重试需要同时满足“故障可恢复”和“重复执行安全”。
- GET、PUT、DELETE 的规范语义可作为起点,具体仍以 API 契约为准。
- POST 写操作要靠服务端幂等键,重试时必须复用同一个键。
- 使用退避、Jitter、次数、Deadline 和取消共同限制重试。
- 重试只能有一个明确所有者,避免层层放大。
问答复盘
Q1:GET 请求是否一定可以安全重试?
答: 不一定。规范语义是只读,但具体接口如果夹带业务写入,就已经破坏了这个前提,仍要以服务端契约为准。
Q2:DELETE 重试后返回 404,说明它不幂等吗?
答: 不说明。幂等关注最终效果,资源重复删除后仍然不存在;每次响应状态可以不同。
Q3:POST 超时后为什么不能直接再发一次?
答: 因为服务端可能已经执行,只是响应丢失。应使用原幂等键重试,或先查询业务状态。
Q4:500 错误都应该自动重试吗?
答: 不应该。只有服务端契约确认它属于瞬时错误,且请求可安全重复时才重试;程序缺陷不会因为多调用几次而修复。
Q5:为什么指数退避还要加入 Jitter?
答: 仅使用相同指数间隔,所有客户端仍可能同时重试。随机抖动可以分散请求波峰。
Q6:重试时为什么必须复用原来的幂等键?
答: 同一个键表示同一次业务意图。换新键会让服务端把重试识别为新的创建或支付操作。
Q7:页面销毁后,读请求还应该继续重试吗?
答: 通常不应该。用户已不再关心页面结果,应取消后续尝试;需要跨页面完成的任务应由更长生命周期的组件管理。