1. 核心干货
核心思路
仅对“瞬时/可恢复”错误执行“指数退避+随机抖动”的智能重试,严禁对业务逻辑错误重试,防止重试风暴压垮服务端。
解决方案流程图(文本版)
[发起API请求] │ ▼ [捕获异常/响应] ──── 成功 ──> [返回数据] │ 失败 ▼ [判断: 是否可重试?] (shouldRetry) │ ├── NO (401/403/404/参数错误) ──> [立即抛出错误/业务降级] │ └── YES (网络断开/5xx/429/超时) │ ▼ [判断: 已达最大重试次数?] │ ├── YES ──> [抛出最终错误/触发熔断/用户提示] │ └── NO │ ▼ [计算延迟时间] = min(基准值 * 2^重试次数, 最大上限) + 随机抖动(Jitter) │ ▼ [异步等待(Sleep)] │ ▼ [进入下一次循环重试]主要矛盾与次要矛盾
- 主要矛盾:用户体验(高可用) vs 服务端稳定性。重试是为了掩盖瞬时故障提升体验,但无序重试会演变为DDoS攻击导致服务雪崩。
- 次要矛盾:
- 重试时机:区分“真故障”与“假故障”(如404重试无意义)。
- 并发冲突:多客户端同步重试导致的“惊群效应”。
- 幂等性安全:非幂等接口(POST/PUT)重试可能导致数据重复或脏写。
2. 结构化答案解析
一、 重试时机判断(Should Retry)
原则:只对网络层和服务端临时状态重试,绝不对客户端业务逻辑错误重试。
| 状态码/错误类型 | 是否重试 | 原因分析 | 补充说明 |
|---|---|---|---|
| Network Error / Timeout | ✅ 是 | DNS失败、断网、超时属瞬时问题 | 需区分是连接超时还是响应超时 |
| 429 Too Many Requests | ✅ 是 | 服务端限流,明确告知稍后重试 | 必须读取Retry-After头,而非盲目退避 |
| 500/502/503/504 | ✅ 是 | 网关错误、服务过载、维护中 | 属于服务端临时不可用 |
| 401 Unauthorized | ❌ 否 | Token过期或无效 | 应触发刷新Token流程或重新登录,而非重试原请求 |
| 403 Forbidden | ❌ 否 | 权限不足 | 重试无法改变权限状态 |
| 404 Not Found | ❌ 否 | 资源不存在 | 接口地址错误或资源已删除 |
| 400 Bad Request | ❌ 否 | 参数校验失败 | 需修改代码或输入,重试必败 |
⚠️ 关键补充:
- 429处理:生产环境中,若服务端返回了
Retry-After响应头,优先级高于自定义的指数退避算法。- 幂等性检查:对于
POST、PATCH等非幂等方法,默认不应自动重试,除非业务层保证了幂等键(Idempotency-Key),否则会导致重复下单、重复扣款。- AbortController:重试时必须支持取消。若用户跳转页面或组件卸载,必须终止所有未完成的重试请求,防止内存泄漏和无效IO。
二、 重试策略
原则:给服务端喘息时间,打散并发流量。
指数退避
- 公式:Delay=Base×2AttemptDelay = Base \times 2^{Attempt}Delay=Base×2Attempt
- 目的:随着失败次数增加,等待时间呈几何级数增长,避免持续高频冲击。
- 补充上限:必须设置最大延迟上限(如30秒),防止等待时间过长导致用户体验崩坏。
随机抖动
- 公式:FinalDelay=CalculatedDelay+Random(−Range,+Range)FinalDelay = CalculatedDelay + Random(-Range, +Range)FinalDelay=CalculatedDelay+Random(−Range,+Range)
- 目的:解决惊群效应。当P0故障恢复瞬间,若无Jitter,数千客户端会在同一毫秒发起请求,瞬间再次压垮服务。
- 优化方案:推荐使用Full Jitter算法(
random(0, base * 2^n))比简单的加减抖动分布更均匀。
三、 边界场景与工程化落地
- 全局vs局部:重试逻辑应封装在HTTP Client拦截器(如Axios Interceptor)中,而非每个业务函数里手写。
- 用户感知:重试期间应保持Loading态或静默;仅在最终失败时才Toast提示用户。
- 监控埋点:必须记录重试次数、耗时、最终结果。若某接口重试率突增,应触发告警。
- SSR环境:Node.js端重试策略应与浏览器端不同(服务端重试容忍度更低,超时更短,避免阻塞渲染)。
3. 完整示例代码(TypeScript + Axios)
以下代码增加了幂等检查、Retry-After支持、AbortSignal支持和最大延迟上限。
/** * 生产级请求重试配置接口 */interfaceRetryConfig{maxRetries:number;// 最大重试次数baseDelay:number;// 基础延迟(ms)maxDelay:number;// 最大延迟上限(ms),防止等待过久retryOnStatusCodes:number[];// 允许重试的状态码白名单}constDEFAULT_CONFIG:RetryConfig={maxRetries:3,baseDelay:1000,maxDelay:30000,// 最长不超过30秒retryOnStatusCodes:[429,500,502,503,504]};/** * 判断当前错误是否值得重试 * 【核心修正】:增加了对 AbortError 的排除,被主动取消的请求绝不重试 */functionshouldRetry(error:any,config:RetryConfig):boolean{// 1. 如果请求已被取消(如路由切换),直接放弃if(error.name==='CanceledError'||error.code==='ERR_CANCELED'){returnfalse;}// 2. 网络层错误(无response)通常可重试if(!error.response){returntrue;}conststatus=error.response.status;// 3. 检查是否在白名单内returnconfig.retryOnStatusCodes.includes(status);}/** * 计算带抖动的指数退避延迟 * 【核心优化】:支持 Retry-After 头 + Full Jitter + Max Cap */functioncalculateDelay(attempt:number,error:any,config:RetryConfig):number{// 优先级1:尊重服务端的 Retry-After 指令constretryAfter=error.response?.headers?.['retry-after'];if(retryAfter){constseconds=parseInt(retryAfter,10);if(!isNaN(seconds))returnMath.min(seconds*1000,config.maxDelay);}// 优先级2:指数退避 + Full Jitter// Full Jitter: random(0, min(cap, base * 2^attempt))// 相比简单的 ±jitter,Full Jitter 能更彻底地打散请求constexponentialDelay=config.baseDelay*Math.pow(2,attempt);constcappedDelay=Math.min(exponentialDelay,config.maxDelay);// 生成 [0, cappedDelay] 之间的随机整数constjitteredDelay=Math.floor(Math.random()*(cappedDelay+1));returnjitteredDelay;}/** * 异步睡眠函数,支持 AbortSignal 中断 * 【关键补充】:防止组件卸载后定时器仍在运行 */functionsleep(ms:number,signal?:AbortSignal):Promise<void>{returnnewPromise((resolve,reject)=>{if(signal?.aborted)returnreject(signal.reason);consttimer=setTimeout(resolve,ms);// 监听取消信号,清理定时器signal?.addEventListener('abort',()=>{clearTimeout(timer);reject(signal.reason);},{once:true});});}/** * 生产级 Fetch/Axios 重试封装 * @param requestFn 实际执行请求的异步函数 * @param config 重试配置 * @param signal AbortSignal 用于外部取消 */exportasyncfunctionrequestWithRetry<T>(requestFn:()=>Promise<T>,config:Partial<RetryConfig>={},signal?:AbortSignal):Promise<T>{constmergedConfig={...DEFAULT_CONFIG,...config};letlastError:any;for(letattempt=0;attempt<=mergedConfig.maxRetries;attempt++){try{// 每次重试前检查是否已被外部取消if(signal?.aborted)throwsignal.reason;returnawaitrequestFn();}catch(error:any){lastError=error;// 1. 判断是否还有重试机会 & 错误是否可重试constisLastAttempt=attempt===mergedConfig.maxRetries;if(isLastAttempt||!shouldRetry(error,mergedConfig)){throwerror;// 不可重试或已达上限,直接抛出}// 2. 计算智能延迟时间constdelayMs=calculateDelay(attempt,error,mergedConfig);console.warn(`[Request Retry] Attempt${attempt+1}/${mergedConfig.maxRetries}failed.`+`Retrying in${delayMs}ms...`,error.message);// 3. 等待,且支持中途取消awaitsleep(delayMs,signal);}}// 理论上不会走到这里,作为兜底throwlastError;}4. 更好的解决方案补充
除了客户端重试,高级工程师还应提出架构层面的替代/增强方案:
- 服务端幂等键:
- 客户端生成唯一UUID放入Header,服务端缓存该Key的处理结果。即使客户端重试10次,服务端也只执行一次业务逻辑。这是解决POST重试安全性的根本方案。
- 网关层重试:
- 将重试逻辑下沉到Nginx/Kong/APISIX。优势:统一管控、减少客户端复杂度、可对内网微服务做更激进的重试。劣势:对用户不透明,需注意超时叠加。
- 断路器模式:
- 重试是“乐观策略”,熔断是“悲观策略”。当错误率超过阈值(如50%),直接打开熔断器拒绝请求,一段时间后半开探测。重试+熔断才是完整的容错体系。
- 请求去重:
- 在重试之前,先检查是否有相同URL+Params的请求正在进行中。若有,直接复用Promise,避免发出多个重复请求。
5. 🏆 满分答案总结(面试话术)
“面试官您好,关于API请求重试,我认为不能简单地用for循环实现,而需要构建一个兼顾用户体验与服务端安全的智能容错机制。我的回答分为四个层次:
第一,精准判断重试时机。只对网络抖动、5xx服务端错误和429限流进行重试;对401/403/404等业务错误直接失败。特别要注意,非幂等的POST请求默认不重试,除非有幂等键保障;且必须优先遵守服务端返回的
Retry-After头。第二,采用指数退避+随机抖动策略。使用Base×2nBase \times 2^nBase×2n递增延迟给服务端恢复时间,同时加入Full Jitter随机因子,彻底避免多客户端同步重试引发的‘惊群效应’导致二次雪崩。并且要设置最大延迟上限,防止用户等待过久。
第三,工程化健壮性保障。重试必须支持
AbortController取消,防止页面切换后的内存泄漏;重试逻辑应封装在拦截器层而非业务层;重试期间保持静默,仅在最终失败时给用户反馈;同时要做好重试指标的监控埋点。第四,架构级补充。客户端重试只是最后一道防线。更优的方案是在服务端实现幂等键保证数据安全,在网关层统一配置重试策略,并结合熔断器模式,在错误率过高时快速失败,形成‘重试+熔断+幂等’的立体防御体系。
以上就是我对生产级请求重试机制的完整思考。”