小Cの已经记不起来的博客

React 项目中 熔断 的设计取舍

起因是前段时间后端一个服务抽风,接口大面积超时。当时前端页面的表现相当难看:一个列表页里三四个组件各自发请求,全部挂在那儿等超时,loading 转了半天,最后弹出一排报错 toast,用户手一抖刷新了页面,又是一轮。控制台里红色的失败请求刷屏,服务器那边也跟着陪葬。那天我就觉得,这事儿必须在前端加个熔断,不然下次还是这副德行。

熔断本来是微服务里的概念,搬到前端来,第一件要想清楚的事是:它到底保护谁。想明白这点,后面一半的取舍就不用纠结了。前端熔断保护不了服务器,每个用户浏览器里各有一份状态,服务器该挨打还是挨打。它保护的是当前这个用户的体验,让他在后端已经出问题的时候,别再对着转圈和报错干耗,顺便少发点注定失败的请求,算是给服务器减一点点负。

熔断逻辑放哪一层

第一个决定是代码放哪。我一开始在组件里做,每个组件自己维护失败计数,试了两天就放弃了——太散,十几个组件写十几遍不说,组件之间互相不知道对方已经把接口熔了,等于白熔。挪到全局状态管理里也试过,结果和业务 store 缠在一起,改起来乱七八糟。

最后老老实实放回请求层,包在 axios 封装里,所有请求统一过一道。核心逻辑其实没几行:

class CircuitBreaker {
  failures = 0;
  threshold = 5;         // 连续失败几次就熔断
  cooldown = 30 * 1000;  // 熔断多久
  openedAt = 0;

  canRequest() {
    if (this.failures < this.threshold) return true;
    // 冷却过了,把接下来这次请求当探测
    return Date.now() - this.openedAt > this.cooldown;
  }

  onSuccess() {
    this.failures = 0;
  }

  onFailure() {
    this.failures++;
    if (this.failures >= this.threshold) {
      this.openedAt = Date.now();
    }
  }
}

这是示意,真正的实现里我还加了个简单状态机,不过骨架就这些。

什么才算失败

这是整个设计里最容易踩坑的地方,我就结结实实踩了一脚。第一版我把所有非 2xx 都算失败,结果有个接口改了路径没人告诉我,一直返回 404,五次之后整个模块被熔断,连那些本来好好的接口也跟着遭殃,排查的时候一脸懵。后来才想明白:404、401、403 这种,是服务器明确答复你“服务活着,是你请求有问题”,不该计入;真正该数的只有超时、网络错误、502/503 这种“后端可能真不行了”的。

阈值上我们最初想搞失败率滑动窗口,写得还挺得意,回头一看纯属过度设计,单个用户的请求量本来就少,窗口都填不满,还不如连续失败次数来得直接。

半开和恢复

主流做法是冷却时间一到,自动发一个探测请求过去,成功了就闭合。我们没有这么做,选了懒探测:冷却期过后不主动发任何请求,等下一次真实的业务请求进来,把它当探测,成了就恢复,败了就重新计时。理由很简单,自动探测要养定时器、要处理探测请求自己的上下文,代码复杂一截,收益却不大——反正探测成功之后,也得等用户下一步操作才有实际意义。没准儿在强实时的场景里这套思路不适用,但对我们这种偏后台的系统,懒探测完全够用了。

和 React Query 打架

这个坑值得单独拎出来说。项目里请求是 React Query 发的,它自带 retry,默认重试三次。叠在熔断外面,等于失败次数被放大三倍,熔断的触发时机完全对不上预期,一开始我还以为是阈值设大了,调了半天没反应才反应过来是这回事。最后把全局 retry 关掉,重试的活儿交给半开探测去干,职责才算清楚:React Query 管缓存和状态,熔断管“这个请求该不该发出去”。

另外 enabled 必须接上熔断状态,不然熔断期间组件一挂载、useEffect 一跑,请求照样漏出去:

const { data } = useQuery({
  queryKey: ['orders', id],
  queryFn: fetchOrders,
  retry: false,
  enabled: breaker.canRequest(),
});

熔断打开时,用户看到什么

这块纠结得最久。直接甩一个错误页肯定不行,白屏更不行。最后做的方案是降级:继续读缓存数据,页面顶上加一条横幅,写明“数据可能不是最新的”,同时禁用提交类的按钮。别小看这条横幅,没有它,用户会默认屏幕上就是最新数据,表单填了一半提交失败才回来骂你,比直接看到报错还糟。

最后的取舍

回头看,真正值得配熔断的就是那几个核心接口:列表、详情、提交。低频的边缘接口犯不着折腾,统一走兜底提示就行。熔断状态我们也没往 sessionStorage 里存,刷新页面就重置,一开始觉得这样不严谨,后来反而觉得挺好:用户都主动刷新了,说明愿意再给后端一次机会,那就从干净的状态开始吧。

评论

还没有评论。

发表评论

提交后评论将经过自动审核,审核通过后公开展示。

未在播放