← 返回文章列表

API 幂等、重试与超时:让失败变得可控

API避坑

超时:三层时间都要设

只设一个「总超时」往往不够。通常分三层:连接超时(1–3 秒,用于建立 TCP/TLS)、读取超时(按接口 P99 设定)、整体重试预算(所有重试累计不超过上限)。没有预算约束时,一次抖动可能引发数分钟的重复请求,反而把下游压垮。

重试:只重试「可能成功」的失败

失败类型是否重试建议
连接超时、502 / 503 / 504适合指数退避 + 抖动
429(限流)谨慎遵循 Retry-After,并限制次数
408 请求超时视场景必须配合幂等键
400 / 401 / 403 / 404 / 422不要请求本身有问题,重试无意义
500谨慎仅在确认是瞬时故障时

指数退避与抖动

// 第 n 次重试的等待时间(毫秒),带随机抖动以避免重试风暴
const base = 200, cap = 10000;
const wait = Math.min(cap, base * 2 ** attempt);
const jitter = Math.random() * wait * 0.3;
await sleep(wait + jitter);

没有抖动的退避会让所有客户端在同一时刻重试,形成重试风暴;抖动把重试分散到一个时间窗口内,显著降低峰值。

幂等:让重试变得安全

幂等指同一请求执行多次与执行一次的结果相同。缺少幂等保证时,重试可能造成重复扣款、重复下单或重复发消息。常见做法:

  1. 客户端为每次业务操作生成幂等键(如 UUID),随请求一起发送;
  2. 服务端记录「幂等键 → 首次结果」,重复请求直接返回该结果而不重复执行;
  3. 幂等键设置有效期(如 24 小时),避免存储无限增长;
  4. 写接口尽量设计为「设置某值」而非「增加某量」(例如 setBalance 优于 addBalance)。

四个常见坑

  • 重试放大流量:没有次数上限与整体预算,一次抖动会被放大成雪崩;
  • 多层同时重试:网关层重试 3 次、应用层再重试 3 次,实际请求变成 9 次;
  • 对非幂等接口重试:直接造成资金或消息重复;
  • 超时设置过长:连接被占满、线程池耗尽,故障扩散得更快。

可观测性与排查

为每次调用带上请求 ID(可用 UUID 生成),并记录尝试次数、耗时与最终状态;出问题时先看「重试次数分布」与「耗时分布」,通常能立刻区分是网络抖动、下游变慢,还是重试策略本身不合理。

动手试试:UUID 生成(生成请求 ID 与幂等键)

超时预算与重试预算

分布式调用中,超时与重试必须当作预算来统一分配,而不是每个服务各拍一个数字。

  1. 自下而上计算超时:先确定最外层可接受的响应时间,再逐层分配,保证上游超时大于下游超时之和,否则上游会在下游仍在处理时就放弃并重试,放大负载。
  2. 重试次数要有限且递减:重试次数与退避时间应随层数递减,越靠近入口重试次数越少,避免一次抖动被多层放大成历史级流量。
  3. 只重试可重试的错误:参数错误、鉴权失败等确定性失败重试没有意义,反而增加负载;只有连接失败、超时与部分服务端错误才值得重试。
  4. 区分读与写的重试策略:读请求可自动重试;写请求必须依赖幂等键,否则重试会产生重复副作用。
  5. 记录重试指标:重试率、重试成功率与重试引入的额外延迟都应上报。重试率长期偏高说明依赖本身不稳定,需要从根因解决。

幂等键的设计细节

幂等键应由客户端生成并携带业务语义,服务端在有效期内保存处理结果,重复请求直接返回首次结果。需要注意键的存储成本与过期策略,以及并发情况下同一键的请求要串行化处理,避免两次都执行到一半。