重试机制设计的核心要点与实战优化策略

设计师青霞 优化 阅读 608
赞 16 收藏
二维码
手机扫码查看
反馈

先说结论:我一般选封装函数 + AbortController

重试机制这玩意儿,说简单也简单,说复杂也能折腾你一整天。我在好几个项目里都碰过类似需求——接口偶尔抖一下,用户点个按钮没反应,刷新页面又好了。这种问题不解决,客服投诉能塞满邮箱。

重试机制设计的核心要点与实战优化策略

我试过不少方案:从最原始的手动 setTimeout 递归,到用第三方库 like axios-retry,再到自己封装带退避策略的 fetch 重试函数。最后发现,最灵活、最可控的,还是自己写一层逻辑,配合 AbortController 控制超时和中断。

当然,如果你项目里已经用了 axios,那 axios-retry 确实省事,但我踩过坑:它对并发请求的控制不够细,加上 CancelToken 已经 deprecated,改起来还挺烦。所以我现在基本不用它了。

三种我用过的方案长啥样

下面这三个是我实战中真正上过线的方案,不是理论派那种“可以这么做”。我都跑过压测,也都在线上被用户疯狂点击验证过。

1. 最土但最直观:setTimeout + 计数器

function retryFetch(url, options = {}, maxRetries = 3, delay = 1000) {
  let attempt = 0

  const makeRequest = () => {
    return fetch(url, options)
      .then(res => {
        if (!res.ok) throw new Error(HTTP ${res.status})
        return res.json()
      })
      .catch(err => {
        attempt++
        if (attempt >= maxRetries) {
          console.error(达到最大重试次数,放弃:, err)
          return Promise.reject(err)
        }

        console.log(请求失败,第 ${attempt} 次重试...)
        setTimeout(makeRequest, delay)
      })
  }

  return makeRequest()
}

这个写法看着挺顺,但有个大问题:不能中断!如果用户快速点了五次,每个都会开启独立的定时器,最后可能同时发起十几条请求。而且 delay 固定,网络差的时候反而更雪崩。

我之前在一个表单提交场景用了这招,结果用户手滑连点,后台直接被打爆。后来加了个 loading 锁才缓解,但治标不治本。

2. Axios + axios-retry(曾经的真爱)

import axios from 'axios'
import axiosRetry from 'axios-retry'

axiosRetry(axios, {
  retries: 3,
  retryDelay: (retryCount) => {
    return retryCount * 1000 // 指数退避?自己算吧
  },
  retryCondition: (error) => {
    // 只对网络错误和 5xx 重试
    return !error.response || (error.response.status >= 500 && error.response.status < 600)
  }
})

// 使用
axios.get('https://jztheme.com/api/data')
  .then(...)
  .catch(...)

一开始真香。配置一次,全局生效,省心。但问题是:太粗暴了。你想针对某个特定接口设置不同的重试次数?难。想在重试时更新 token?得 hook 进去,代码变得很绕。

还有个致命伤:CancelToken 废弃后,AbortController 接入不顺畅。我折腾了半天才发现 axios-retry 内部还没完全适配,某些情况下 abort 不生效,内存泄漏警告都出来了。

所以现在除非是内部小工具项目,否则我不推荐用这组合。

3. 我现在主力用的:自封装 fetch + AbortController + 退避算法

async function fetchWithRetry(
  url,
  options = {},
  { maxRetries = 3, baseDelay = 1000, shouldRetry = null } = {}
) {
  let lastError

  for (let i = 0; i <= maxRetries; i++) {
    const controller = new AbortController()
    const timeoutId = setTimeout(() => controller.abort(), 8000) // 超时 8s

    // 动态退避:第 n 次重试等待 baseDelay * 2^(n-1)
    if (i > 0) {
      const delay = baseDelay * Math.pow(2, i - 1)
      await new Promise(resolve => setTimeout(resolve, delay))
      console.log(第 ${i} 次重试,延迟 ${delay}ms)
    }

    try {
      const res = await fetch(url, {
        ...options,
        signal: controller.signal
      })

      clearTimeout(timeoutId)

      if (!res.ok) {
        lastError = new Error(HTTP ${res.status}: ${res.statusText})
        if (shouldRetry && !shouldRetry(res)) {
          break
        }
        continue
      }

      return await res.json()
    } catch (err) {
      if (err.name === 'AbortError') {
        lastError = new Error(请求超时或被取消)
      } else {
        lastError = err
      }

      // 网络异常一律重试(除非超过次数)
      if (i < maxRetries) continue
    }
  }

  return Promise.reject(lastError)
}

// 使用示例
fetchWithRetry('https://jztheme.com/api/data', {
  method: 'GET',
  headers: { 'Content-Type': 'application/json' }
}, {
  maxRetries: 2,
  baseDelay: 1000,
  shouldRetry: (res) => res.status === 503 // 只对 503 重试
})
.then(data => console.log(data))
.catch(err => console.error(最终失败:, err.message))

这个版本我现在所有新项目都在用。虽然多写了十几行,但自由度高到飞起:

  • 可配置退避策略(指数/固定/随机)
  • 支持超时中断
  • 可定制重试条件
  • Promise 链清晰,调试容易

最重要的是,不会像 axios-retry 那样暗地里搞事情。每一行都是我自己写的,出问题我也知道去哪改。

谁更灵活?谁更省事?

看场景。

如果你只是想给整个项目的 API 请求统一加个“最多重两次”的兜底,且不怕全局副作用,那 axios-retry 确实快。但你要做好后期难以拆分的心理准备。

如果你想精细控制每个请求的行为,比如“登录失败不重试”“轮询接口要指数退避”,那就别图省事了,老老实实封装一个函数。

至于那个最原始的 setTimeout 版本?我已经把它当反面教材收藏了。除了 demo 演示,生产环境真不适合用。

还有一个隐藏成本:维护性。团队新人看到 axios-retry 的配置,不一定知道背后发生了什么;但看到一个叫 fetchWithRetry 的函数,大概率能猜出来它是干啥的。

我的选型逻辑

我现在是这么定规矩的:

  • 新项目一律用原生 fetch + 自封装重试函数
  • 老 axios 项目,如果改动成本大,就继续用 axios-retry,但加个 wrapper 控制范围
  • 需要取消请求的场景,必须用 AbortController,其他都不考虑

我还把上面那个 fetchWithRetry 抽成了 utils/request.js,根据不同环境调整 baseDelay 和 maxRetries。甚至可以根据 NODE_ENV 开关重试功能,方便本地调试。

这里注意我踩过好几次坑:退避时间别设太短。有一次我把 baseDelay 设成 500ms,结果服务端刚好也在扩容,连续三次 504,客户端疯狂重试,形成雪崩。后来改成最小 1s 起步,指数增长,才稳住。

以上是我的对比总结,有不同看法欢迎评论区交流

重试机制看着小,但真要做得靠谱,细节一堆。我说的这套也不是银弹,比如还没加上 jitter(随机抖动)来防重试风暴,后续可能会补。

也有同事用 RxJS 的 retryWhen 操作符实现过类似功能,写法更函数式,但我个人觉得学习成本偏高,中小型项目有点杀鸡用牛刀。

总之,别迷信第三方库。很多时候,十几行代码自己封装一个,比引入一个包更安全、更可控。尤其是涉及网络请求这种高频操作,越透明越好。

以上是我踩坑后的总结,希望对你有帮助。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论