高并发下裸调用 fetch 或 axios 必须解耦重试、退避、取消、熔断四者;需绑定 AbortController.signal 实现可取消重试,采用带±10%抖动的指数退避(base×2^retryCount),仅对5xx、AbortError等服务端错误重试,最大3~5次。

高并发下裸调用 fetch 或 axios 必然压垮下游,不是加个 retry 选项就能解决的——必须把重试、退避、取消、熔断四者解耦并分层控制。
为什么不能直接用 setTimeout + for 循环实现重试
手动管理定时器在高并发下极易失控:setTimeout 不可取消,组件卸载后仍执行 setState 会报 Can't perform a React state update on an unmounted component;多个并发请求共享同一个重试计数器,导致实际重试次数远超配置;更关键的是,它完全不感知 AbortSignal,无法响应上游中断(比如用户切换页面、请求被主动 cancel)。
- 所有重试逻辑必须绑定
AbortController.signal,每次fetch都传入该 signal - 重试间隔不能硬编码,必须是
baseDelay * 2^retryCount,且需加 ±10% 随机抖动(jitter),否则大量客户端会在同一时刻发起重试,触发雪崩 - 最大重试次数建议设为
3~5,超过即透出错误,避免无限沉默重试掩盖真实问题
如何用 fetch 封装一个可取消、带抖动的指数退避 request 函数
不依赖第三方库,适合已弃用 axios 或需按 URL 分组定制策略的场景。核心是把重试逻辑抽成纯函数,副作用(如 fetch 调用)只发生在最内层。
- 用
AbortController控制整个重试链路:首次创建 controller,每次重试前检查signal.aborted,若已中止则直接 reject - 退避时间计算示例:
const delay = Math.min(base * Math.pow(2, retryCount) * (1 + (Math.random() - 0.5) * 0.2), maxDelay) - 只对可重试错误触发重试:
error?.name === 'AbortError'(网络中断)、error?.response?.status >= 500、error?.code === 'ECONNREFUSED',但绝不重试400类客户端错误
async function request(url, options = {}, { base = 1000, max = 10000, maxRetries = 3 } = {}) {
let lastError;
for (let i = 0; i try {
const res = await fetch(url, { ...options, signal });
if (res.ok) return res;
if (![500, 502, 503, 504].includes(res.status)) throw new Error(`HTTP ${res.status}`);
} catch (err) {
lastError = err;
if (i === maxRetries) break;
const delay = Math.min(base * Math.pow(2, i) * (1 + (Math.random() - 0.5) * 0.2), max);
await new Promise(r => setTimeout(r, delay));
continue;
}
} throw lastError; }
重试必须和断路器分层组合,且顺序不能错
很多团队把 retry 和 circuitBreaker 套在同一层,结果断路器统计的是“重试后的最终结果”,而非“单次请求成败”——这会让熔断阈值失效。正确做法是:先封装重试逻辑,再用断路器包裹整个重试块。
- Go 中用
gobreaker时,每次循环开头必须调cb.Allow(),失败后立刻cb.Failure(),成功才cb.Success();backoff.ExponentialBackOff的InitialInterval和MaxInterval必须显式设置,否则默认1s/60s完全不适用高频服务 - Java 中用
resilience4j,RetryConfig用IntervalFunction.ofExponentialBackoff,CircuitBreakerConfig设置failureRateThreshold: 50、waitDurationInOpenState: 60s、slidingWindowSize: 10 - 断路器窗口大小(
slidingWindowSize)直接影响统计精度:太小(如 5)易误熔断,太大(如 100)则响应迟钝,建议从 20 开始调优
非幂等请求绝不能自动重试,必须显式标记
POST、PUT、DELETE 等写操作,若未在服务端实现幂等性(如通过 Idempotency-Key 请求头),自动重试会导致重复扣款、库存超卖等严重问题。网关层必须强制校验。
- 默认只对
GET、HEAD、OPTIONS自动重试;其他方法必须在请求配置中显式声明idempotent: true,否则跳过重试逻辑 - 网关应自动注入
X-Idempotency-Key(如 UUID v4)和X-Retry-Count头,供下游服务识别和去重 - 重试时需保留原始请求体(
body)、请求头(headers)和 method,不能因重试丢失任何上下文
真正难的不是实现指数退避,而是让重试行为可观察、可中断、可审计——每个重试动作都该打日志(含 url、status、retryCount、delayMs),且所有延迟必须走统一的调度器,不能散落在各处 setTimeout。










