使用 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++) {
复制代码 在这个例子中:
- 我们尝试从 https://api.example.com/data 获取数据。
- 如果请求失败,最多会重试 3 次,每次等待时间依次为 1 秒、2 秒、4 秒。
- 如果所有尝试都失败,最终会抛出错误并记录到日志。
输出示例
假设一次请求出现错误,以下是输出:- 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();
复制代码 最适用的场景
- 网络请求中容易由于瞬时错误失败,比如 API 接口短暂不可用。
- 需要提高异步操作的鲁棒性,而不是因为一次失败就终止流程。
总结
使用 TypeScript + Axios,借助指数退避实现了自动重试的功能。该解决方案兼容性强、扩展性高,适用于任何异步任务场景。通过适当配置 maxRetries 与 baseDelay,不仅可以提升操作的容错性,还能够避免在高压力环境下对系统资源的过度消耗。
如果你需要灵活的异步请求重试机制,这将是一个理想的实现。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |