Skip to content

接口请求重试机制

概述

接口请求重试机制是前端容错处理的重要手段,用于处理网络波动服务端临时故障超时等场景,提升应用的稳定性和用户体验。


一、基础实现

简单重试

javascript
/**
 * 基础重试函数
 * @param {Function} fn - 需要重试的异步函数
 * @param {number} retries - 最大重试次数
 * @param {number} delay - 重试间隔(毫秒)
 */
async function retry(fn, retries = 3, delay = 1000) {
  for (let i = 0; i < retries; i++) {
    try {
      return await fn()
    } catch (error) {
      // 最后一次重试失败,抛出错误
      if (i === retries - 1) {
        throw error
      }

      console.warn(`第 ${i + 1} 次请求失败,${delay}ms 后重试...`)

      // 等待后重试
      await new Promise(resolve => setTimeout(resolve, delay))
    }
  }
}

// 使用示例
const data = await retry(
  () => fetch('/api/data').then(res => res.json()),
  3,
  1000
)

带配置的重试

javascript
/**
 * 可配置的重试函数
 */
async function retryRequest(fn, options = {}) {
  const {
    retries = 3,              // 最大重试次数
    delay = 1000,             // 初始延迟
    maxDelay = 30000,         // 最大延迟
    backoff = 'exponential',  // 退避策略: 'fixed' | 'linear' | 'exponential'
    retryOn = [],             // 指定重试的错误码/状态码
    onRetry = null,           // 重试回调
    shouldRetry = null        // 自定义重试条件
  } = options

  let lastError

  for (let attempt = 0; attempt <= retries; attempt++) {
    try {
      return await fn(attempt)
    } catch (error) {
      lastError = error

      // 检查是否应该重试
      if (attempt === retries) {
        break
      }

      // 自定义重试条件
      if (shouldRetry && !shouldRetry(error, attempt)) {
        break
      }

      // 检查错误码
      if (retryOn.length > 0) {
        const errorCode = error.status || error.code || error.response?.status
        if (!retryOn.includes(errorCode)) {
          break
        }
      }

      // 计算延迟时间
      const waitTime = calculateDelay(delay, attempt, backoff, maxDelay)

      // 重试回调
      onRetry?.({
        error,
        attempt: attempt + 1,
        delay: waitTime
      })

      // 等待
      await sleep(waitTime)
    }
  }

  throw lastError
}

/**
 * 计算延迟时间
 */
function calculateDelay(baseDelay, attempt, strategy, maxDelay) {
  let delay

  switch (strategy) {
    case 'fixed':
      // 固定延迟
      delay = baseDelay
      break

    case 'linear':
      // 线性增长: 1s, 2s, 3s, 4s...
      delay = baseDelay * (attempt + 1)
      break

    case 'exponential':
      // 指数退避: 1s, 2s, 4s, 8s...
      delay = baseDelay * Math.pow(2, attempt)
      break

    default:
      delay = baseDelay
  }

  // 添加抖动,避免同时重试
  const jitter = delay * 0.2 * Math.random()
  delay = delay + jitter

  return Math.min(delay, maxDelay)
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms))
}

// 使用示例
const data = await retryRequest(
  () => fetch('/api/data').then(res => {
    if (!res.ok) throw new Error(`HTTP ${res.status}`)
    return res.json()
  }),
  {
    retries: 3,
    delay: 1000,
    backoff: 'exponential',
    retryOn: [408, 500, 502, 503, 504],  // 只在这些状态码时重试
    onRetry: ({ attempt, delay }) => {
      console.log(`第 ${attempt} 次重试,等待 ${delay}ms`)
    }
  }
)

二、指数退避算法

什么是指数退避

javascript
/**
 * 指数退避 (Exponential Backoff)
 *
 * 原理: 每次重试的等待时间呈指数增长
 * 公式: delay = baseDelay * 2^attempt
 *
 * 示例 (baseDelay = 1000ms):
 * - 第1次重试: 1000ms
 * - 第2次重试: 2000ms
 * - 第3次重试: 4000ms
 * - 第4次重试: 8000ms
 *
 * 优点:
 * 1. 给服务器恢复时间
 * 2. 避免大量请求同时重试造成雪崩
 * 3. 减少不必要的网络请求
 */

带抖动的指数退避

javascript
/**
 * 指数退避 + 抖动 (Exponential Backoff with Jitter)
 *
 * 为什么需要抖动:
 * - 多个客户端同时失败,如果都在相同时间重试,会造成"惊群效应"
 * - 添加随机抖动可以分散重试时间
 *
 * 常见抖动策略:
 * 1. Full Jitter: delay = random(0, baseDelay * 2^attempt)
 * 2. Equal Jitter: delay = baseDelay * 2^attempt / 2 + random(0, baseDelay * 2^attempt / 2)
 * 3. Decorrelated Jitter: delay = min(maxDelay, random(baseDelay, prevDelay * 3))
 */

class ExponentialBackoff {
  constructor(options = {}) {
    this.baseDelay = options.baseDelay || 1000
    this.maxDelay = options.maxDelay || 30000
    this.maxRetries = options.maxRetries || 5
    this.jitterType = options.jitterType || 'full'  // 'none' | 'full' | 'equal' | 'decorrelated'

    this.attempt = 0
    this.prevDelay = this.baseDelay
  }

  // 获取下次延迟时间
  nextDelay() {
    if (this.attempt >= this.maxRetries) {
      return null  // 超过最大重试次数
    }

    const exponentialDelay = this.baseDelay * Math.pow(2, this.attempt)
    let delay

    switch (this.jitterType) {
      case 'none':
        delay = exponentialDelay
        break

      case 'full':
        // Full Jitter: [0, exponentialDelay]
        delay = Math.random() * exponentialDelay
        break

      case 'equal':
        // Equal Jitter: exponentialDelay/2 + [0, exponentialDelay/2]
        delay = exponentialDelay / 2 + Math.random() * (exponentialDelay / 2)
        break

      case 'decorrelated':
        // Decorrelated Jitter
        delay = Math.random() * (this.prevDelay * 3 - this.baseDelay) + this.baseDelay
        this.prevDelay = delay
        break

      default:
        delay = exponentialDelay
    }

    this.attempt++

    return Math.min(delay, this.maxDelay)
  }

  // 重置
  reset() {
    this.attempt = 0
    this.prevDelay = this.baseDelay
  }

  // 是否还能重试
  canRetry() {
    return this.attempt < this.maxRetries
  }
}

// 使用示例
async function fetchWithBackoff(url, options = {}) {
  const backoff = new ExponentialBackoff({
    baseDelay: 1000,
    maxDelay: 30000,
    maxRetries: 5,
    jitterType: 'full'
  })

  while (true) {
    try {
      const response = await fetch(url, options)

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`)
      }

      return response.json()

    } catch (error) {
      const delay = backoff.nextDelay()

      if (delay === null) {
        throw error  // 超过最大重试次数
      }

      console.log(`请求失败,${Math.round(delay)}ms 后重试 (第 ${backoff.attempt} 次)`)
      await sleep(delay)
    }
  }
}

三、Axios 重试拦截器

基础拦截器实现

javascript
import axios from 'axios'

// 创建 axios 实例
const instance = axios.create({
  baseURL: '/api',
  timeout: 10000
})

// 重试配置
const retryConfig = {
  retries: 3,
  retryDelay: 1000,
  retryCondition: (error) => {
    // 网络错误或 5xx 错误时重试
    return !error.response || (error.response.status >= 500 && error.response.status <= 599)
  }
}

// 响应拦截器
instance.interceptors.response.use(
  response => response,
  async error => {
    const config = error.config

    // 初始化重试计数
    config.__retryCount = config.__retryCount || 0

    // 检查是否应该重试
    if (
      config.__retryCount >= retryConfig.retries ||
      !retryConfig.retryCondition(error)
    ) {
      return Promise.reject(error)
    }

    config.__retryCount++

    // 计算延迟
    const delay = retryConfig.retryDelay * Math.pow(2, config.__retryCount - 1)

    console.log(`请求 ${config.url} 失败,${delay}ms 后进行第 ${config.__retryCount} 次重试`)

    // 等待后重试
    await new Promise(resolve => setTimeout(resolve, delay))

    return instance(config)
  }
)

export default instance

完整的重试插件

javascript
/**
 * Axios 重试插件
 */
function axiosRetry(axiosInstance, defaultOptions = {}) {
  const defaults = {
    retries: 3,
    retryDelay: (retryCount) => retryCount * 1000,  // 支持函数
    retryCondition: isRetryableError,
    shouldResetTimeout: false,
    onRetry: null
  }

  // 合并默认配置
  const globalConfig = { ...defaults, ...defaultOptions }

  axiosInstance.interceptors.response.use(null, async (error) => {
    const config = error.config

    // 获取重试配置(请求级别配置优先)
    const retryConfig = {
      ...globalConfig,
      ...config.retry
    }

    // 初始化重试状态
    const currentState = config.__retryState || {}
    config.__retryState = currentState

    currentState.retryCount = currentState.retryCount || 0

    // 判断是否应该重试
    const shouldRetry = retryConfig.retryCondition(error) &&
                        currentState.retryCount < retryConfig.retries

    if (!shouldRetry) {
      return Promise.reject(error)
    }

    currentState.retryCount++

    // 计算延迟
    const delay = typeof retryConfig.retryDelay === 'function'
      ? retryConfig.retryDelay(currentState.retryCount, error)
      : retryConfig.retryDelay

    // 重试回调
    retryConfig.onRetry?.(currentState.retryCount, error, config)

    // 重置超时
    if (retryConfig.shouldResetTimeout && config.timeout) {
      config.timeout = config.timeout
    }

    // 延迟后重试
    await new Promise(resolve => setTimeout(resolve, delay))

    return axiosInstance(config)
  })
}

/**
 * 判断错误是否可重试
 */
function isRetryableError(error) {
  // 网络错误
  if (!error.response) {
    return true
  }

  // 幂等方法的 5xx 错误
  const idempotentMethods = ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE']
  const isIdempotent = idempotentMethods.includes(error.config.method?.toUpperCase())

  if (isIdempotent && error.response.status >= 500) {
    return true
  }

  // 特定状态码
  const retryableStatusCodes = [408, 429, 500, 502, 503, 504]
  if (retryableStatusCodes.includes(error.response.status)) {
    return true
  }

  return false
}

// 使用示例
const api = axios.create({
  baseURL: '/api',
  timeout: 10000
})

axiosRetry(api, {
  retries: 3,
  retryDelay: (retryCount) => {
    // 指数退避
    return Math.pow(2, retryCount) * 1000
  },
  retryCondition: (error) => {
    // 自定义重试条件
    return isRetryableError(error)
  },
  onRetry: (retryCount, error, config) => {
    console.log(`[${config.url}] 第 ${retryCount} 次重试`)
  }
})

// 请求级别配置
api.get('/data', {
  retry: {
    retries: 5,  // 覆盖全局配置
    retryDelay: 2000
  }
})

四、Fetch 重试封装

基础封装

javascript
/**
 * 带重试的 Fetch
 */
async function fetchWithRetry(url, options = {}) {
  const {
    retries = 3,
    retryDelay = 1000,
    retryOn = [408, 500, 502, 503, 504],
    timeout = 10000,
    onRetry,
    ...fetchOptions
  } = options

  let lastError

  for (let attempt = 0; attempt <= retries; attempt++) {
    const controller = new AbortController()
    const timeoutId = setTimeout(() => controller.abort(), timeout)

    try {
      const response = await fetch(url, {
        ...fetchOptions,
        signal: controller.signal
      })

      clearTimeout(timeoutId)

      // 检查是否需要重试
      if (!response.ok && retryOn.includes(response.status)) {
        throw new Error(`HTTP ${response.status}`)
      }

      return response

    } catch (error) {
      clearTimeout(timeoutId)
      lastError = error

      // 最后一次尝试,抛出错误
      if (attempt === retries) {
        break
      }

      // 检查是否是可重试的错误
      const isRetryable = error.name === 'AbortError' ||  // 超时
                          error.name === 'TypeError' ||    // 网络错误
                          error.message.startsWith('HTTP')

      if (!isRetryable) {
        break
      }

      // 计算延迟(指数退避)
      const delay = retryDelay * Math.pow(2, attempt)

      onRetry?.({
        attempt: attempt + 1,
        error,
        delay
      })

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

  throw lastError
}

// 使用示例
try {
  const response = await fetchWithRetry('/api/data', {
    method: 'GET',
    retries: 3,
    retryDelay: 1000,
    timeout: 5000,
    onRetry: ({ attempt, delay }) => {
      console.log(`重试 ${attempt},等待 ${delay}ms`)
    }
  })

  const data = await response.json()
  console.log(data)

} catch (error) {
  console.error('请求最终失败:', error)
}

高级封装

javascript
/**
 * 高级 Fetch 封装
 * 支持: 重试、超时、取消、进度
 */
class FetchClient {
  constructor(baseURL = '', defaultOptions = {}) {
    this.baseURL = baseURL
    this.defaults = {
      retries: 3,
      retryDelay: 1000,
      backoff: 'exponential',
      timeout: 10000,
      retryOn: [408, 429, 500, 502, 503, 504],
      ...defaultOptions
    }
  }

  async request(url, options = {}) {
    const config = {
      ...this.defaults,
      ...options,
      url: this.baseURL + url
    }

    const {
      retries,
      retryDelay,
      backoff,
      timeout,
      retryOn,
      onRetry,
      onProgress,
      ...fetchOptions
    } = config

    let lastError
    let lastResponse

    for (let attempt = 0; attempt <= retries; attempt++) {
      const controller = new AbortController()
      let timeoutId

      try {
        // 设置超时
        if (timeout) {
          timeoutId = setTimeout(() => controller.abort(), timeout)
        }

        const response = await fetch(config.url, {
          ...fetchOptions,
          signal: controller.signal
        })

        clearTimeout(timeoutId)
        lastResponse = response

        // 成功响应
        if (response.ok) {
          return response
        }

        // 检查是否需要重试
        if (!retryOn.includes(response.status)) {
          return response
        }

        throw new Error(`HTTP ${response.status}`)

      } catch (error) {
        clearTimeout(timeoutId)
        lastError = error

        // 最后一次尝试
        if (attempt === retries) {
          break
        }

        // 检查是否可重试
        if (!this.isRetryable(error)) {
          break
        }

        // 计算延迟
        const delay = this.calculateDelay(retryDelay, attempt, backoff)

        // 重试回调
        onRetry?.({
          attempt: attempt + 1,
          totalRetries: retries,
          error,
          delay
        })

        await this.sleep(delay)
      }
    }

    // 如果有响应但不成功,返回响应
    if (lastResponse) {
      return lastResponse
    }

    throw lastError
  }

  isRetryable(error) {
    // 超时
    if (error.name === 'AbortError') {
      return true
    }

    // 网络错误
    if (error.name === 'TypeError') {
      return true
    }

    // HTTP 错误
    if (error.message.startsWith('HTTP')) {
      return true
    }

    return false
  }

  calculateDelay(baseDelay, attempt, strategy) {
    let delay

    switch (strategy) {
      case 'fixed':
        delay = baseDelay
        break
      case 'linear':
        delay = baseDelay * (attempt + 1)
        break
      case 'exponential':
        delay = baseDelay * Math.pow(2, attempt)
        break
      default:
        delay = baseDelay
    }

    // 添加抖动
    const jitter = delay * 0.2 * Math.random()
    return delay + jitter
  }

  sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms))
  }

  // 便捷方法
  get(url, options) {
    return this.request(url, { ...options, method: 'GET' })
  }

  post(url, data, options) {
    return this.request(url, {
      ...options,
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        ...options?.headers
      },
      body: JSON.stringify(data)
    })
  }

  put(url, data, options) {
    return this.request(url, {
      ...options,
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json',
        ...options?.headers
      },
      body: JSON.stringify(data)
    })
  }

  delete(url, options) {
    return this.request(url, { ...options, method: 'DELETE' })
  }
}

// 使用示例
const api = new FetchClient('/api', {
  retries: 3,
  timeout: 10000
})

// GET 请求
const response = await api.get('/users', {
  onRetry: ({ attempt, delay }) => {
    console.log(`第 ${attempt} 次重试`)
  }
})

// POST 请求
await api.post('/users', { name: 'John' })

五、Vue 3 组合式函数

javascript
// composables/useRequest.js
import { ref, shallowRef } from 'vue'

export function useRequest(requestFn, options = {}) {
  const {
    immediate = false,
    retries = 3,
    retryDelay = 1000,
    backoff = 'exponential',
    onSuccess,
    onError,
    onRetry
  } = options

  const data = shallowRef(null)
  const error = ref(null)
  const loading = ref(false)
  const retryCount = ref(0)

  async function execute(...args) {
    loading.value = true
    error.value = null
    retryCount.value = 0

    for (let attempt = 0; attempt <= retries; attempt++) {
      try {
        const result = await requestFn(...args)
        data.value = result
        onSuccess?.(result)
        return result

      } catch (e) {
        error.value = e

        if (attempt === retries) {
          onError?.(e)
          break
        }

        retryCount.value = attempt + 1

        const delay = calculateDelay(retryDelay, attempt, backoff)
        onRetry?.({ attempt: attempt + 1, error: e, delay })

        await sleep(delay)
      }
    }

    loading.value = false
    throw error.value
  }

  function reset() {
    data.value = null
    error.value = null
    loading.value = false
    retryCount.value = 0
  }

  // 立即执行
  if (immediate) {
    execute()
  }

  return {
    data,
    error,
    loading,
    retryCount,
    execute,
    reset
  }
}

function calculateDelay(baseDelay, attempt, strategy) {
  switch (strategy) {
    case 'fixed':
      return baseDelay
    case 'linear':
      return baseDelay * (attempt + 1)
    case 'exponential':
      return baseDelay * Math.pow(2, attempt)
    default:
      return baseDelay
  }
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms))
}

// 使用示例
// <script setup>
// import { useRequest } from '@/composables/useRequest'
//
// const { data, loading, error, execute, retryCount } = useRequest(
//   () => fetch('/api/data').then(res => res.json()),
//   {
//     retries: 3,
//     retryDelay: 1000,
//     backoff: 'exponential',
//     onRetry: ({ attempt }) => {
//       console.log(`第 ${attempt} 次重试`)
//     }
//   }
// )
//
// // 手动执行
// await execute()
// </script>
//
// <template>
//   <div v-if="loading">加载中... (重试: {{ retryCount }})</div>
//   <div v-else-if="error">{{ error.message }}</div>
//   <div v-else>{{ data }}</div>
// </template>

六、React Hook

jsx
import { useState, useCallback, useRef, useEffect } from 'react'

function useRequest(requestFn, options = {}) {
  const {
    manual = true,
    retries = 3,
    retryDelay = 1000,
    backoff = 'exponential',
    onSuccess,
    onError,
    onRetry
  } = options

  const [data, setData] = useState(null)
  const [error, setError] = useState(null)
  const [loading, setLoading] = useState(false)
  const [retryCount, setRetryCount] = useState(0)

  const mountedRef = useRef(true)

  useEffect(() => {
    mountedRef.current = true
    return () => {
      mountedRef.current = false
    }
  }, [])

  const execute = useCallback(async (...args) => {
    setLoading(true)
    setError(null)
    setRetryCount(0)

    for (let attempt = 0; attempt <= retries; attempt++) {
      try {
        const result = await requestFn(...args)

        if (mountedRef.current) {
          setData(result)
          setLoading(false)
          onSuccess?.(result)
        }

        return result

      } catch (e) {
        if (!mountedRef.current) return

        if (attempt === retries) {
          setError(e)
          setLoading(false)
          onError?.(e)
          throw e
        }

        setRetryCount(attempt + 1)

        const delay = calculateDelay(retryDelay, attempt, backoff)
        onRetry?.({ attempt: attempt + 1, error: e, delay })

        await sleep(delay)
      }
    }
  }, [requestFn, retries, retryDelay, backoff, onSuccess, onError, onRetry])

  const reset = useCallback(() => {
    setData(null)
    setError(null)
    setLoading(false)
    setRetryCount(0)
  }, [])

  // 非手动模式,自动执行
  useEffect(() => {
    if (!manual) {
      execute()
    }
  }, [manual, execute])

  return {
    data,
    error,
    loading,
    retryCount,
    execute,
    reset
  }
}

function calculateDelay(baseDelay, attempt, strategy) {
  switch (strategy) {
    case 'fixed':
      return baseDelay
    case 'linear':
      return baseDelay * (attempt + 1)
    case 'exponential':
      return baseDelay * Math.pow(2, attempt)
    default:
      return baseDelay
  }
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms))
}

// 使用示例
function UserList() {
  const { data, loading, error, retryCount, execute } = useRequest(
    () => fetch('/api/users').then(res => res.json()),
    {
      manual: false,  // 自动执行
      retries: 3,
      retryDelay: 1000,
      backoff: 'exponential',
      onRetry: ({ attempt, delay }) => {
        console.log(`第 ${attempt} 次重试,等待 ${delay}ms`)
      }
    }
  )

  if (loading) {
    return <div>加载中... {retryCount > 0 && `(重试 ${retryCount})`}</div>
  }

  if (error) {
    return (
      <div>
        <p>错误: {error.message}</p>
        <button onClick={execute}>重试</button>
      </div>
    )
  }

  return (
    <ul>
      {data?.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )
}

七、重试策略最佳实践

1. 幂等性考虑

javascript
/**
 * 幂等性 (Idempotency)
 *
 * 幂等请求: 多次执行结果相同
 * - GET: 幂等 ✓
 * - PUT: 幂等 ✓
 * - DELETE: 幂等 ✓
 * - POST: 非幂等 ✗
 * - PATCH: 非幂等 ✗
 *
 * 非幂等请求需要特殊处理
 */

function shouldRetry(error, config) {
  // 非幂等方法默认不重试
  const nonIdempotentMethods = ['POST', 'PATCH']
  const method = config.method?.toUpperCase()

  if (nonIdempotentMethods.includes(method)) {
    // 只在网络错误(请求未到达服务器)时重试
    return !error.response
  }

  // 幂等方法可以重试
  return true
}

// 使用幂等键保证安全
async function safePost(url, data, options = {}) {
  const idempotencyKey = options.idempotencyKey || generateUUID()

  return fetchWithRetry(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Idempotency-Key': idempotencyKey  // 服务端需要支持
    },
    body: JSON.stringify(data),
    ...options
  })
}

2. 断路器模式

javascript
/**
 * 断路器模式 (Circuit Breaker)
 *
 * 状态:
 * - CLOSED: 正常,允许请求
 * - OPEN: 熔断,拒绝请求
 * - HALF_OPEN: 半开,允许部分请求测试
 *
 * 作用: 防止对已知故障服务持续请求
 */

class CircuitBreaker {
  constructor(options = {}) {
    this.failureThreshold = options.failureThreshold || 5    // 失败阈值
    this.successThreshold = options.successThreshold || 3    // 恢复阈值
    this.timeout = options.timeout || 30000                  // 熔断时间

    this.state = 'CLOSED'
    this.failureCount = 0
    this.successCount = 0
    this.lastFailureTime = null
  }

  async execute(fn) {
    // 检查状态
    if (this.state === 'OPEN') {
      // 检查是否可以进入半开状态
      if (Date.now() - this.lastFailureTime >= this.timeout) {
        this.state = 'HALF_OPEN'
        console.log('断路器进入半开状态')
      } else {
        throw new Error('Circuit breaker is OPEN')
      }
    }

    try {
      const result = await fn()
      this.onSuccess()
      return result

    } catch (error) {
      this.onFailure()
      throw error
    }
  }

  onSuccess() {
    this.failureCount = 0

    if (this.state === 'HALF_OPEN') {
      this.successCount++

      if (this.successCount >= this.successThreshold) {
        this.state = 'CLOSED'
        this.successCount = 0
        console.log('断路器关闭')
      }
    }
  }

  onFailure() {
    this.failureCount++
    this.lastFailureTime = Date.now()
    this.successCount = 0

    if (this.failureCount >= this.failureThreshold) {
      this.state = 'OPEN'
      console.log('断路器打开')
    }
  }

  getState() {
    return this.state
  }
}

// 结合重试使用
const breaker = new CircuitBreaker({
  failureThreshold: 5,
  timeout: 30000
})

async function fetchWithCircuitBreaker(url) {
  return breaker.execute(async () => {
    return fetchWithRetry(url, {
      retries: 3,
      retryDelay: 1000
    })
  })
}

3. 请求队列

javascript
/**
 * 请求队列
 * 控制并发数,失败时自动重试
 */

class RequestQueue {
  constructor(options = {}) {
    this.concurrency = options.concurrency || 3
    this.retries = options.retries || 3
    this.retryDelay = options.retryDelay || 1000

    this.queue = []
    this.running = 0
    this.results = new Map()
  }

  add(id, requestFn) {
    return new Promise((resolve, reject) => {
      this.queue.push({
        id,
        requestFn,
        resolve,
        reject,
        retryCount: 0
      })

      this.process()
    })
  }

  async process() {
    while (this.running < this.concurrency && this.queue.length > 0) {
      const task = this.queue.shift()
      this.running++

      this.executeTask(task)
        .then(result => {
          this.results.set(task.id, { success: true, data: result })
          task.resolve(result)
        })
        .catch(error => {
          this.results.set(task.id, { success: false, error })
          task.reject(error)
        })
        .finally(() => {
          this.running--
          this.process()
        })
    }
  }

  async executeTask(task) {
    try {
      return await task.requestFn()
    } catch (error) {
      if (task.retryCount < this.retries) {
        task.retryCount++
        await sleep(this.retryDelay * task.retryCount)
        return this.executeTask(task)
      }
      throw error
    }
  }

  getResults() {
    return this.results
  }
}

// 使用示例
const queue = new RequestQueue({
  concurrency: 3,
  retries: 3
})

// 批量请求
const urls = ['/api/1', '/api/2', '/api/3', '/api/4', '/api/5']

const promises = urls.map((url, index) =>
  queue.add(index, () => fetch(url).then(res => res.json()))
)

const results = await Promise.allSettled(promises)

八、常见面试题

1. 为什么需要请求重试?

点击查看答案

主要原因:

  1. 网络波动:移动网络、弱网环境下连接不稳定
  2. 服务端临时故障:服务重启、负载过高等
  3. 超时问题:网络延迟导致请求超时
  4. 提升用户体验:自动恢复,减少用户手动操作

适用场景:

  • GET 请求(幂等)
  • 网络错误(请求未到达服务器)
  • 5xx 服务端错误
  • 408 请求超时
  • 429 请求过多(需配合退避)

2. 什么是指数退避?为什么要用?

点击查看答案

指数退避 (Exponential Backoff):

每次重试的等待时间呈指数增长,如:1s → 2s → 4s → 8s

为什么使用:

  1. 给服务器恢复时间:如果服务器过载,立即重试会加重负担
  2. 避免惊群效应:大量客户端同时重试会造成雪崩
  3. 减少资源浪费:避免无意义的频繁请求

最佳实践:

  • 配合抖动(Jitter)使用,分散重试时间
  • 设置最大延迟上限
  • 设置最大重试次数

3. POST 请求可以重试吗?

点击查看答案

需要谨慎处理:

POST 请求通常是非幂等的,重试可能导致:

  • 重复创建资源
  • 重复提交订单
  • 数据不一致

安全重试的条件:

  1. 请求未到达服务器(网络错误)
  2. 使用幂等键(Idempotency-Key)
  3. 业务层面保证幂等(如订单号去重)
javascript
// 使用幂等键
headers: {
  'Idempotency-Key': 'unique-request-id'
}

4. 如何避免重试风暴?

点击查看答案

防护措施:

  1. 指数退避 + 抖动:分散重试时间
  2. 断路器模式:失败达到阈值后停止请求
  3. 限制重试次数:设置合理的最大重试次数
  4. 请求队列:控制并发数
  5. 客户端限流:限制请求频率
javascript
// 示例:结合多种策略
const config = {
  retries: 3,
  backoff: 'exponential',
  jitter: true,
  maxDelay: 30000,
  circuitBreaker: {
    failureThreshold: 5,
    timeout: 30000
  }
}

总结

核心要点

  1. 指数退避:避免频繁重试,给服务器恢复时间
  2. 添加抖动:分散重试时间,避免惊群效应
  3. 幂等性:非幂等请求需特殊处理
  4. 断路器:防止对故障服务持续请求
  5. 合理配置:重试次数、延迟、超时都要合理设置

推荐库

  • axios-retry:Axios 重试插件
  • retry:通用重试库
  • p-retry:Promise 重试
  • cockatiel:完整的弹性策略库(重试、断路器、超时等)