1、重试策略基础:为什么需要重试?重试的代价与收益分析

做分布式系统这些年,我见过太多因为一次临时抖动就全盘崩溃的例子。说白了,重试不是锦上添花,而是保命手段。但重试也不是万能的——用错了,反而会把系统拖垮。

这一节我们先聊清楚:重试到底解决了什么问题?代价有多大?值不值得做?

1.1 为什么需要重试?

分布式系统里,故障是常态,不是异常。网络闪断、服务重启、GC暂停、连接池耗尽……这些事每天都在发生。你想想看,一个请求发出去,对方可能正在重启,或者网络刚好抖了一下,这时候直接放弃,太亏了。

我遇到过最典型的场景:一次线上促销活动,订单服务调用库存服务扣减库存。因为数据库连接池短暂打满,库存服务返回了超时。订单服务没有重试,直接报错。结果用户反复提交订单,库存反而被多次扣减——这就是典型的「不重试 vs 乱重试」的坑。

重试的核心价值就两点:

  • 容忍瞬时故障:网络抖动、服务重启、GC暂停等短暂异常,重试一次就能恢复
  • 提升最终成功率:单次失败不意味着永远失败,重试能把成功率从 99% 拉到 99.99%

关键认知:重试不是解决「系统不可用」的方案,而是解决「系统偶尔不可用」的方案。如果服务已经挂了,重试只会加重灾难。

1.2 重试的代价分析

重试不是免费的。每多一次重试,系统就要多消耗一份资源。我习惯把重试的代价分成三类:

代价类型 具体表现 影响范围
资源消耗 CPU、内存、网络带宽、连接数、数据库连接 本服务 + 下游服务
延迟增加 每次重试增加 RTT,用户等待时间成倍增长 用户体验
级联风险 重试流量叠加到下游,可能引发雪崩 整个调用链

举个例子。假设一个服务正常处理请求需要 50ms,QPS 是 1000。如果 10% 的请求需要重试一次,那额外的 QPS 就是 100。这 100 个重试请求会跟正常请求抢资源,导致正常请求的延迟也变高——恶性循环就这么开始了。

我曾经踩过的坑:有一次我把重试次数设成了 3 次,结果下游服务 GC 暂停了 2 秒,所有请求都超时,然后每个请求重试 3 次,流量瞬间变成 4 倍。下游恢复后直接被冲垮,花了半小时才缓过来。从那以后,我定了个规矩:重试次数不超过 2 次,且必须配合退避策略。

1.3 收益分析:什么时候值得重试?

重试的收益取决于两个因素:故障的持续时间和重试的间隔。

如果故障持续 100ms,你等 200ms 再重试,大概率能成功。如果故障持续 10 秒,你等 200ms 重试 3 次,全都会失败——白白浪费资源。

我一般用这个公式来估算收益:

重试成功率 = 1 - (故障持续时间 / 重试间隔) ^ 重试次数

举个例子:

  • 故障持续 200ms,重试间隔 500ms,重试 2 次
  • 第一次重试成功率 ≈ 1 - (200/500) = 60%
  • 第二次重试成功率 ≈ 1 - (200/500)² = 84%
  • 两次重试后总成功率 ≈ 1 - (1-0.6)*(1-0.84) = 93.6%

嗯,这个收益就很可观了。

我的经验法则:如果重试成功率低于 50%,说明故障持续时间太长,或者重试间隔太短。这时候应该考虑降级或熔断,而不是继续重试。

1.4 重试策略的核心权衡

重试策略本质上是在做一道选择题:用多少额外资源,换取多少成功率提升?

我习惯用一张图来理解这个权衡:

重试策略权衡分析 重试策略 收益:提升成功率 代价:资源消耗 容忍瞬时故障 提升最终成功率 减少人工介入 CPU/内存/带宽 延迟增加 级联雪崩风险 核心问题:用多少额外资源换取多少成功率? 答案:取决于故障特征和业务容忍度 重试不是越多越好,也不是越少越好。关键是找到那个「刚刚好」的点。

1.5 什么时候不该重试?

不是所有失败都值得重试。我总结了几个「打死也不重试」的场景:

  • 业务逻辑错误:参数校验失败、权限不足、余额不足——重试一万次也没用
  • 幂等性无法保证:如果重试会导致重复扣款、重复下单,那宁可失败也不能重试
  • 下游明确拒绝:返回 429 Too Many Requests 或 503 Service Unavailable,说明下游已经扛不住了,再重试就是落井下石
  • 超时时间过长:如果请求本身已经等了 5 秒才超时,重试一次又要等 5 秒,用户体验已经崩了

一句话总结:重试只解决「临时性、可恢复的故障」。对于永久性故障,重试是毒药,不是解药。

1.6 重试策略的决策框架

我个人习惯用这个框架来判断要不要重试:

  1. 判断故障类型:是临时抖动还是永久故障?看错误码和异常类型
  2. 评估重试成本:重试一次要消耗多少资源?会增加多少延迟?
  3. 估算成功概率:根据历史数据或故障特征,估算重试成功的可能性
  4. 考虑业务影响:失败和重试失败,哪个对业务伤害更大?
  5. 决定重试参数:次数、间隔、退避策略、超时控制

嗯,这个框架我用了好几年,基本没出过问题。你想想看,如果每次重试前都过一遍这个框架,那些「重试风暴」和「雪崩」的惨案,至少能避免一大半。

我的习惯:在代码里把重试逻辑封装成独立的组件,不要散落在业务代码里。这样改策略的时候只需要改一个地方,而且方便做监控和告警。