指数退避重试的核心是通过随机抖动、最大重试次数和超时熔断避免重试风暴与雪崩,而非简单增加重试次数;需区分4xx/5xx错误类型,仅对网络错误、5xx及Retry-After响应启用退避。
直接上结论:用指数退避重试网关,核心不是“多试几次”,而是让失败请求在系统压力大时主动让出资源,并避免雪崩——关键在退避策略必须带随机抖动、最大重试次数和超时熔断双保险。
setTimeout 简单累加会把后端压垮常见错误是写成 retryDelay = base * 2 ** attempt 然后 setTimeout(fn, retryDelay)。这会导致所有客户端在同一时刻重试(比如第3次都在 800ms 后触发),形成“重试风暴”。尤其在服务短暂不可用后恢复的瞬间,大量请求扎堆打进来,可能直接击穿刚缓过来的后端。
实操建议:
Math.random() * 0.3 左右的随机因子,例如 retryDelay = base * Math.pow(2, attempt) * (1 + Math.random() * 0.3)
60000 ms),防止某次重试卡住 5 分钟才动一下if (circuitBreaker.isOpen()) throw new Error("CIRCUIT_OPEN")
fetch 请求中嵌入退避逻辑的最小可行实现别封装成黑盒函数,要保留对 signal、headers 和响应体处理的控制权。下面这段可直接塞进你的请求工具里:
async function pollWithBackoff(url, options = {}, { base = 1000, maxRetries = 5 } = {}) { let lastError; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), options.timeout || 10000); const res = await fetch(url, { ...options, signal: controller.signal }); clearTimeout(timeoutId); if (!res.ok) throw new Error(`HTTP ${res.status}`); return await res.json(); } catch (err) { lastError = err; if (attempt === maxRetries) break; const jitter = 1 + Math.random() * 0.3; const delay = Math.min(base * Math.pow(2, attempt) * jitter, 60000); await new Promise(r => setTimeout(r, delay)); } } throw lastError;}
注意点:
AbortController 必须每次重试都新建,否则上一次 abort 会影响后续请求timeout 是 per-attempt 的,不是整个轮询周期的总超时await res.json(),不是原始 Response,方便上层直接用数据;若需流式处理或自定义解析,应把 res 传出去不是所有错误都适合指数退避。盲目重试 401 Unauthorized 或 404 Not Found 只会浪费资源;而 503 Service Unavailable、429 Too Many Requests、连接拒绝或超时才真正需要退避。
实操建议:
TypeError)、5xx 和明确标了重试提示的响应(如 Retry-After header)启用退避4xx 中仅特殊处理 408 Request Timeout 和 429;其余一律立即失败Retry-After,优先用它替代计算出的 delay,但依然要叠加 jitter 防止同步attempt、status、delay 到日志,便于排查是否误判了错误类型最易被忽略的是退避策略与业务语义的耦合:比如轮询订单状态,若 3 次重试后仍是 “PROCESSING”,该继续等还是通知用户“处理中”?退避逻辑只管请求节奏,状态语义必须由上层决定——网关不替你判断“多久算太久”。