使用 TypeScript 的指数退避机制包装异步请求
在进行网络请求时,很有可能会遇到某些临时失败,例如网络波动、请求超时、服务器端未响应等。面对这种情况,最好的做法往往是实施一种重试机制,而指数退避(Exponential Backoff) 是一种非常流行且有效的重试策略。它通过递增间隔时间来避免系统过度拥塞,提高成功执行的几率。
本文将基于 TypeScript 和 Axios 构造一个适用于任意 API 请求的通用 Promise 包装函数,并包含支持指数退避重试的功能。
什么是指数退避?
指数退避是通过延迟策略实现的一种算法,其核心是随着重试次数的增加,系统会按照展示增长的方式增加每次重试前等待的时间。公式如下:
delay = baseDelay * (2 ^ attempt)
假如 baseDelay 为 1000 毫秒,第一次重试后等待 1 秒,第二次重试等待 2 秒,第三次等待 4 秒……延迟的时间会迅速增大。
这种机制可以有效减少系统的负载压力,同时也可以应对一些临时性的问题,比如网络波动、瞬时连接错误等。
指数退避的代码实现
我们使用 axios 库作为 HTTP 客户端,通过包装一个请求函数,并在请求失败时实现可配置的重试机制。下面是完整版代码:
/**
* 包装一次请求,支持指数退避的自动重试。
* @param requesrtFn 实际执行请求的函数,应该返回一个 Promise
* @param maxRetries 最大重试次数,默认2次
* @param baseDelay 基础延迟时间,单位毫秒,默认1000ms
* @returns 请求成功时的响应数据
* @throws 最后一次请求失败的错误
*/
async function requestWithRetry<T>(
requestFn: () => Promise<T>,
maxRetries = 2,
baseDelay = 1000
): Promise<T> {
let lastError: any;
// 循环次数:0(初始尝试) + maxRetries(重试次数)
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
// 调用真正的请求函数,若成功则直接返回结果
return await requestFn();
} catch (err) {
// 捕获异常,记录下来以便在全部尝试结束后抛出
lastError = err;
// 判断是否为网络错误
const isNetworkError = axios.isAxiosError(err) &&
(!err.response || // 没有收到服务器响应
err.code === 'ECONNABORTED' || // 超时
err.code === 'ECONNREFUSED'); // 连接被拒绝
// 若是网络错误,且还有剩余重试次数,则执行指数退避
if (isNetworkError && attempt < maxRetries) {
// 计算本次等待时间: baseDelay * 2^attempt
// 第一次失败等待 1 s(baseDelay),第二次失败等待 2 s,第三次失败等待 4 s,以此类推
const delay = baseDelay * Math.pow(2, attempt);
console.log(
`[EverMemOS] Retry attempt ${attempt + 1}/${maxRetries} after ${delay}ms`)
;
// 使用 Promise + setTimeout 实现异步等待
await new Promise(resolve => setTimeout(resolve, delay));
// 继续下一轮循环(再次调用 requestFn)
continue;
}
break;
}
}
// 所有尝试都失败了,抛出最后一次失误给调用方
throw lastError;
}
核心功能拆解
1. 指数退避的实现
指数退避的核心公式是 baseDelay * Math.pow(2, attempt)。代码中:
baseDelay是每次重试的基础等待时间,初始值由调用者传递(默认为 1000 毫秒)。- 每次重试后,等待时间按指数增加,第一次重试等待
baseDelay毫秒,第二次等待baseDelay * 2毫秒,第三次等待baseDelay * 4毫秒。
通过 Promise 与 setTimeout 的结合,实现异步等待:
await new Promise(resolve => setTimeout(resolve, delay));
从而使程序在每次重试之前等待 动态时间间隔。
2. 网络错误的处理逻辑
在重试机制中我们需要区分可重试错误与不可重试错误。事实上,只有网络错误才适合执行重试,例如:
- 超时(HTTP 状态码为
408或ECONNABORTED)。 - 连接被拒绝(
ECONNREFUSED)。 - 服务器未响应(没有
response返回)。
通过 Axios 的 isAxiosError 方法,代码对错误类型进行了详细判断,确保只有网络相关错误被捕获:
const isNetworkError = axios.isAxiosError(err) &&
(!err.response || err.code === 'ECONNABORTED' || err.code === 'ECONNREFUSED');
3. 最大重试次数的控制
为了防止无限重试引发性能问题,该实现使用了 maxRetries 参数以控制最大尝试次数。代码中明确约定了:
- 初次尝试计为第 0 次。
- 重试次数最多不超过
maxRetries。
这里控制重试次数:
for (let attempt = 0; attempt <= maxRetries; attempt++) {
任何超出设定最大次数(如 2 次)的重试都会被终止。
使用示例
可以将这个函数应用到任何需要发送 HTTP 请求的场景中,例如从 API 获取数据:
import axios from 'axios';
const client = axios.create({
baseURL: 'https://api.example.com',
timeout: 5000
});
async function fetchData() {
try {
const response = await requestWithRetry(client, () => client.get('/data'), 3, 1000);
console.log('Data: ', response.data);
} catch (error) {
console.error('Failed to fetch data:', error);
}
}
fetchData();
在这个例子中:
- 我们尝试从
https://api.example.com/data获取数据。 - 如果请求失败,最多会重试 3 次,每次等待时间依次为 1 秒、2 秒、4 秒。
- 如果所有尝试都失败,最终会抛出错误并记录到日志。
输出示例
假设一次请求出现错误,以下是输出:
[Retry] Attempt 1/3 after 1000ms
[Retry] Attempt 2/3 after 2000ms
[Retry] Attempt 3/3 after 4000ms
Failed to fetch data: Error: ECONNABORTED
最适用的场景
- 网络请求中容易由于瞬时错误失败,比如 API 接口短暂不可用。
- 需要提高异步操作的鲁棒性,而不是因为一次失败就终止流程。
总结
使用 TypeScript + Axios,借助指数退避实现了自动重试的功能。该解决方案兼容性强、扩展性高,适用于任何异步任务场景。通过适当配置 maxRetries 与 baseDelay,不仅可以提升操作的容错性,还能够避免在高压力环境下对系统资源的过度消耗。
如果你需要灵活的异步请求重试机制,这将是一个理想的实现。
浙公网安备 33010602011771号