请求重试机制实现解析
在网络请求中,由于网络波动、服务器过载等原因,请求失败是常见的情况。一个健壮的重试机制可以显著提高系统的可靠性。本文将深入分析一个功能完善的请求重试方法的实现原理。
核心概念
为什么需要重试机制?
| 场景 | 说明 |
|---|---|
| 网络抖动 | 短暂的网络中断导致请求超时 |
| 服务器过载 | 服务器临时繁忙返回 503 |
| 限流保护 | 触发 API 限流返回 429 |
| 临时故障 | 数据库连接池耗尽等瞬时错误 |
重试策略对比
| 策略 | 特点 | 适用场景 |
|---|---|---|
| 固定间隔 | 每次重试间隔相同 | 简单场景,负载均匀 |
| 指数退避 | 间隔时间指数增长 | 服务器过载,需要恢复时间 |
| 指数退避 + 抖动 | 在指数退避基础上加随机波动 | 高并发场景,避免重试风暴 |
类型定义
typescript/** * 重试条件函数类型:判断是否需要重试(返回 true 则重试) * @param error 本次请求的错误 * @param attempt 当前重试次数(从 1 开始,首次失败为 1) */ type RetryCondition = (error: unknown, attempt: number) => boolean; /** * 重试间隔函数类型:返回下次重试的延迟时间(毫秒) * @param attempt 当前重试次数(从 1 开始) */ type RetryDelay = (attempt: number) => number; /** * 请求重试配置项 */ interface RetryConfig { /** 最大重试次数(默认 3 次),0 表示不重试 */ maxRetries?: number; /** 重试基础间隔(毫秒)| 间隔函数(默认 1000ms) */ delay?: number | RetryDelay; /** 重试条件(默认所有错误都重试) */ retryCondition?: RetryCondition; /** 是否开启指数退避(默认 false),开启后 delay 为基数 */ exponentialBackoff?: boolean; /** 随机抖动范围(百分比,0-1,默认 0.5):比如 0.5 表示 ±50% 随机波动 */ jitterFactor?: number; }
类型设计要点
| 类型 | 作用 |
|---|---|
RetryCondition | 灵活控制哪些错误需要重试,哪些直接失败 |
RetryDelay | 支持自定义延迟计算逻辑 |
RetryConfig | 统一配置项,所有参数都有合理默认值 |
核心实现
typescript/** * 通用请求重试方法(带随机抖动的指数退避) * @param requestFn 待重试的请求函数(必须返回 Promise) * @param config 重试配置 * @returns Promise<T> 请求成功的返回值 */ async function requestWithRetry<T>( requestFn: (controller?: AbortController) => Promise<T>, config: RetryConfig = {}, ): Promise<T> { // 默认配置:加入随机抖动因子(±50%) const { maxRetries = 3, delay = 1000, retryCondition = () => true, exponentialBackoff = false, jitterFactor = 0.5, } = config; // 创建 AbortController(用于取消后续重试请求) const abortController = new AbortController(); let lastError: unknown; // 生成带随机抖动的延迟时间 const getJitteredDelay = (baseDelay: number): number => { // 计算抖动范围:baseDelay * jitterFactor const jitter = baseDelay * jitterFactor; // 生成 [-jitter, +jitter] 范围内的随机值 const randomJitter = Math.random() * 2 * jitter - jitter; // 确保延迟时间 ≥ 0(避免负数) return Math.max(0, baseDelay + randomJitter); }; // 定义递归重试函数 const attemptRequest = async (attempt: number): Promise<T> => { try { // 执行请求,传入 AbortController 供请求函数使用 const result = await requestFn(abortController); return result; } catch (error) { lastError = error; // 1. 达到最大重试次数:抛出最终错误 if (attempt >= maxRetries) { abortController.abort(); throw new Error( `请求失败(已重试 ${maxRetries} 次): ${(error as Error).message}`, { cause: lastError }, ); } // 2. 判断是否满足重试条件 if (!retryCondition(error, attempt)) { abortController.abort(); throw new Error( `请求失败(不满足重试条件): ${(error as Error).message}`, { cause: error }, ); } // 3. 计算基础重试间隔 let baseDelayMs = typeof delay === "function" ? delay(attempt) : exponentialBackoff ? delay * Math.pow(2, attempt - 1) // 指数退避:1s → 2s → 4s... : delay; // 4. 加入随机抖动,分散重试时间 const finalDelayMs = getJitteredDelay(baseDelayMs); console.log( `第 ${attempt} 次重试失败,${finalDelayMs.toFixed(0)}ms 后重试...`, ); // 5. 延迟后重试 await new Promise((resolve) => setTimeout(resolve, finalDelayMs)); return attemptRequest(attempt + 1); } }; // 启动第一次请求(attempt 从 1 开始) return attemptRequest(1); }
关键设计解析
1. 指数退避算法
typescript// 指数退避计算公式 baseDelayMs = delay * Math.pow(2, attempt - 1); // 示例:基础间隔 1000ms // 第 1 次重试:1000 * 2^0 = 1000ms (1s) // 第 2 次重试:1000 * 2^1 = 2000ms (2s) // 第 3 次重试:1000 * 2^2 = 4000ms (4s) // 第 4 次重试:1000 * 2^3 = 8000ms (8s)
为什么使用指数退避?
- 给服务器更多恢复时间
- 避免持续高频请求加重服务器负担
- 符合网络拥塞控制的最佳实践
2. 随机抖动(Jitter)
typescriptconst getJitteredDelay = (baseDelay: number): number => { const jitter = baseDelay * jitterFactor; const randomJitter = Math.random() * 2 * jitter - jitter; return Math.max(0, baseDelay + randomJitter); }; // 示例:baseDelay = 2000ms, jitterFactor = 0.5 // jitter = 2000 * 0.5 = 1000 // randomJitter 范围:[-1000, +1000] // 最终延迟范围:[1000ms, 3000ms]
为什么需要随机抖动?
| 问题 | 没有抖动 | 有抖动 |
|---|---|---|
| 重试风暴 | 所有客户端同时重试 | 重试时间分散 |
| 服务器压力 | 瞬时流量尖峰 | 流量平滑分布 |
| 成功率 | 相互竞争资源 | 减少冲突 |
3. AbortController 集成
typescriptconst abortController = new AbortController(); // 传递给请求函数,支持取消请求 const result = await requestFn(abortController); // 达到最大重试次数或不满足条件时取消 abortController.abort();
使用场景:
typescriptasync function fetchWithAbort(controller?: AbortController) { return fetch("/api/data", { signal: controller?.signal, // 支持取消 }); }
4. 错误链(Error Cause)
typescriptthrow new Error( `请求失败(已重试 ${maxRetries} 次): ${(error as Error).message}`, { cause: lastError }, // ES2022 特性:保留原始错误 );
好处:
- 保留完整的错误上下文
- 便于调试和日志分析
- 支持错误链追踪
使用示例
基础用法
typescript// 简单重试:固定间隔 const result = await requestWithRetry(() => fetch("/api/data"), { maxRetries: 3, delay: 1000, });
指数退避 + 随机抖动
typescriptconst result = await requestWithRetry(() => fetch("/api/data"), { maxRetries: 5, delay: 1000, exponentialBackoff: true, // 开启指数退避 jitterFactor: 0.3, // ±30% 随机抖动 });
自定义重试条件
typescriptconst result = await requestWithRetry(() => fetch("/api/data"), { maxRetries: 3, retryCondition: (error, attempt) => { // 只重试网络错误和 5xx 错误 if (error instanceof TypeError) return true; // 网络错误 if (error instanceof Response) { return error.status >= 500; // 服务器错误 } return false; // 其他错误不重试 }, });
自定义延迟函数
typescriptconst result = await requestWithRetry(() => fetch("/api/data"), { maxRetries: 4, delay: (attempt) => { // 自定义延迟:1s, 3s, 5s, 10s const delays = [1000, 3000, 5000, 10000]; return delays[attempt - 1] || 10000; }, });
完整示例
typescript// 模拟一个可能失败的请求 async function mockApiRequest( controller?: AbortController, ): Promise<{ data: string }> { const isSuccess = Math.random() > 0.6; if (isSuccess) { return { data: "请求成功" }; } else { throw new Error("网络超时"); } } // 使用重试方法 async function test() { try { const result = await requestWithRetry(mockApiRequest, { maxRetries: 3, delay: 1000, exponentialBackoff: true, jitterFactor: 0.3, }); console.log("最终结果:", result); } catch (error) { console.error("最终失败:", error); } } test();
最佳实践
1. 合理设置最大重试次数
typescript// ❌ 错误:重试次数过多 { maxRetries: 10; } // 可能导致长时间等待 // ✅ 正确:根据场景设置 { maxRetries: 3; } // 一般 API 请求 { maxRetries: 5; } // 关键业务请求 { maxRetries: 1; } // 实时性要求高的请求
2. 区分可重试和不可重试错误
typescript// ✅ 可重试的错误 // - 网络超时 (ETIMEDOUT) // - 连接被拒绝 (ECONNREFUSED) // - 503 Service Unavailable // - 429 Too Many Requests // ❌ 不应重试的错误 // - 400 Bad Request(参数错误) // - 401 Unauthorized(认证失败) // - 404 Not Found(资源不存在) // - 422 Unprocessable Entity(业务逻辑错误)
3. 配合断路器使用
typescriptclass CircuitBreaker { private failures = 0; private lastFailure = 0; private readonly threshold = 5; private readonly resetTimeout = 30000; async execute<T>(fn: () => Promise<T>): Promise<T> { // 检查断路器状态 if (this.isOpen()) { throw new Error("断路器已打开,请稍后重试"); } try { const result = await fn(); this.reset(); return result; } catch (error) { this.recordFailure(); throw error; } } private isOpen(): boolean { if (this.failures >= this.threshold) { const now = Date.now(); if (now - this.lastFailure < this.resetTimeout) { return true; } this.reset(); } return false; } private recordFailure(): void { this.failures++; this.lastFailure = Date.now(); } private reset(): void { this.failures = 0; } }
常见面试题
Q1: 为什么需要随机抖动?
答: 随机抖动(Jitter)主要解决"惊群效应"问题。当大量客户端同时失败并使用相同的退避策略时,它们会在相同的时间点重试,导致服务器瞬时压力激增。加入随机抖动后,重试时间会分散开,避免重试风暴,提高整体成功率。
Q2: 指数退避的时间复杂度是什么?
答: 假设基础延迟为 d,最大重试次数为 n,则总等待时间为:
$$T = d \times (2^0 + 2^1 + ... + 2^{n-1}) = d \times (2^n - 1)$$
时间复杂度为 O(2^n),这就是为什么需要设置合理的最大重试次数。
Q3: 如何避免重试导致的重复请求问题?
答:
- 幂等性设计:确保接口支持幂等操作(如使用请求 ID)
- AbortController:在重试前取消前一个请求
- 服务端去重:基于请求 ID 或时间窗口去重
Q4: 这个实现还有哪些可以优化的地方?
答:
- 添加最大延迟上限(避免指数退避导致过长等待)
- 支持重试回调(onRetry)用于日志记录
- 集成断路器模式
- 支持取消重试(通过 AbortSignal)
总结
"一个健壮的重试机制是构建可靠分布式系统的基础。本文实现的
requestWithRetry方法涵盖了指数退避、随机抖动、条件重试、请求取消等核心特性。
理解重试机制的关键在于:指数退避给服务器恢复时间、随机抖动避免重试风暴、条件判断区分可重试错误。在实际项目中,建议配合断路器、超时控制等机制一起使用,构建完整的容错体系。



