实战指南

请求重试机制实现解析

深入理解 JavaScript 中的请求重试策略,包括指数退避、随机抖动等高级特性的实现原理

2026年1月25日12 分钟1477 阅读
JavaScriptTypeScript异步编程网络请求错误处理
请求重试机制实现解析

请求重试机制实现解析

在网络请求中,由于网络波动、服务器过载等原因,请求失败是常见的情况。一个健壮的重试机制可以显著提高系统的可靠性。本文将深入分析一个功能完善的请求重试方法的实现原理。

核心概念

为什么需要重试机制?

场景说明
网络抖动短暂的网络中断导致请求超时
服务器过载服务器临时繁忙返回 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)

typescript
const 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 集成

typescript
const abortController = new AbortController();

// 传递给请求函数,支持取消请求
const result = await requestFn(abortController);

// 达到最大重试次数或不满足条件时取消
abortController.abort();

使用场景:

typescript
async function fetchWithAbort(controller?: AbortController) {
  return fetch("/api/data", {
    signal: controller?.signal, // 支持取消
  });
}

4. 错误链(Error Cause)

typescript
throw new Error(
  `请求失败(已重试 ${maxRetries} 次): ${(error as Error).message}`,
  { cause: lastError }, // ES2022 特性:保留原始错误
);

好处:

  • 保留完整的错误上下文
  • 便于调试和日志分析
  • 支持错误链追踪

使用示例

基础用法

typescript
// 简单重试:固定间隔
const result = await requestWithRetry(() => fetch("/api/data"), {
  maxRetries: 3,
  delay: 1000,
});

指数退避 + 随机抖动

typescript
const result = await requestWithRetry(() => fetch("/api/data"), {
  maxRetries: 5,
  delay: 1000,
  exponentialBackoff: true, // 开启指数退避
  jitterFactor: 0.3, // ±30% 随机抖动
});

自定义重试条件

typescript
const 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; // 其他错误不重试
  },
});

自定义延迟函数

typescript
const 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. 配合断路器使用

typescript
class 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: 如何避免重试导致的重复请求问题?

答:

  1. 幂等性设计:确保接口支持幂等操作(如使用请求 ID)
  2. AbortController:在重试前取消前一个请求
  3. 服务端去重:基于请求 ID 或时间窗口去重

Q4: 这个实现还有哪些可以优化的地方?

答:

  1. 添加最大延迟上限(避免指数退避导致过长等待)
  2. 支持重试回调(onRetry)用于日志记录
  3. 集成断路器模式
  4. 支持取消重试(通过 AbortSignal)

总结

"

一个健壮的重试机制是构建可靠分布式系统的基础。本文实现的 requestWithRetry 方法涵盖了指数退避、随机抖动、条件重试、请求取消等核心特性。

理解重试机制的关键在于:指数退避给服务器恢复时间随机抖动避免重试风暴条件判断区分可重试错误。在实际项目中,建议配合断路器、超时控制等机制一起使用,构建完整的容错体系。

文章标签

# JavaScript# TypeScript# 异步编程# 网络请求# 错误处理
返回首页