使用 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 毫秒。

通过 PromisesetTimeout 的结合,实现异步等待:

await new Promise(resolve => setTimeout(resolve, delay));

从而使程序在每次重试之前等待 动态时间间隔


2. 网络错误的处理逻辑

在重试机制中我们需要区分可重试错误与不可重试错误。事实上,只有网络错误才适合执行重试,例如:

  • 超时(HTTP 状态码为 408ECONNABORTED)。
  • 连接被拒绝(ECONNREFUSED)。
  • 服务器未响应(没有 response 返回)。

通过 AxiosisAxiosError 方法,代码对错误类型进行了详细判断,确保只有网络相关错误被捕获:

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,借助指数退避实现了自动重试的功能。该解决方案兼容性强、扩展性高,适用于任何异步任务场景。通过适当配置 maxRetriesbaseDelay,不仅可以提升操作的容错性,还能够避免在高压力环境下对系统资源的过度消耗。

如果你需要灵活的异步请求重试机制,这将是一个理想的实现。

posted on 2026-02-15 22:22  快乐的乙炔  阅读(3)  评论(0)    收藏  举报