4. 重试可能带来的问题:幂等性与雪崩归纳总结
重试机制不是万能的。用好了,它能扛住瞬态故障;用不好,它会加倍伤害你的系统。
我在刚做接口重试时,就踩过一个大坑。当时一个支付接口超时了,我直接配置了 3 次重试。结果用户被扣了 4 次钱。嗯,那次事故让我彻底记住了两个字:幂等。另一个坑更吓人——重试风暴把下游服务打挂了,然后整个链路像多米诺骨牌一样塌掉。说白了,这就是雪崩。
这一章,我把这两个问题拆开揉碎讲清楚。你最好先背下来:不做幂等就去重试 = 制造脏数据;不做防护就去重试 = 引爆雪崩。
4.1 幂等性问题——同一个请求,不能造成多次副作用
什么是幂等?你用同一个请求调一次和调 N 次,系统状态应该保持一致。读操作天然幂等,但写操作就不一定了。尤其是支付、下单、转账这类操作。
为什么重试会破坏幂等?因为重试本质上就是把同一个请求又发了一遍。如果被调方(比如支付网关)已经成功处理了第一次请求,只是响应超时了,第二次重试就会再执行一次扣款。用户就多付了钱。
解决幂等性问题的常见做法:
- 全局幂等键(Idempotency Key):每个请求带上唯一 ID。服务端根据这个 ID 去重。第一次执行后把结果缓存起来,后续相同 ID 的请求直接返回缓存结果。我推荐用 UUID 或雪花 ID。
- 去重表 + 唯一索引:在数据库里建一张去重表,把幂等键设为唯一索引。插入成功说明是第一次请求,插入失败说明已处理过。注意,去重表要单独建,别跟业务表混在一起,避免锁竞争。
- 状态机驱动:对于订单等有明确状态流转的实体,可以用状态机确保每个状态只能向前走一次。比如“待支付→已支付→已发货”,重试时如果发现已是“已支付”,直接返回成功,不再执行扣款。
我个人习惯,在涉及钱的接口上,三者同时使用:上游传幂等键,下游用去重表兜底,业务状态机做最终防护。三重保险,基本就不会出事。
4.2 雪崩问题——重试风暴才是真正的隐形杀手
如果说幂等性是接口级别的“内伤病”,雪崩就是集群级别的“瘟疫病”。
先看一个典型场景。假设你的 A 服务依赖 B 服务。某天 B 服务响应变慢,从 50ms 涨到 1s。A 服务的重试策略是“超时 500ms 重试 2 次”。好了,现在每次请求在 A 端都要等待:第一次请求 500ms 超时,重试 1 次等 500ms 再超时,重试第 2 次等 500ms……一个请求就耗了 1.5s 以上。更可怕的是,A 服务的线程池很快被这些等待中的请求填满。新的请求进来直接排队,排队也超时,然后也重试……
你想想看,这像不像滚雪球?重试放大了负载,负载拖垮了下游,下游的故障又通过重试反向传播到上游。最终整个调用链全部瘫痪。这就是重试风暴导致的雪崩。
防范雪崩的常见手段:
| 策略 | 做法 | 我推荐的适用场景 |
|---|---|---|
| 指数退避(Exponential Backoff) | 每次重试间隔加倍。例如第一次等 200ms,第二次等 400ms,第三次等 800ms。 | 所有需要重试的地方,这是标配 |
| 限制最大重试次数 | 通常不超过 2~3 次。超过就快速失败,不要无限重试。 | 所有场景 |
| 熔断器(Circuit Breaker) | 当失败率达到阈值,直接断开支路,快速返回失败,不再重试。 | 关键链路上的依赖调用 |
| 限流(Rate Limiting) | 在上游对下游的调用做并发控制,防止瞬时重试风暴。 | 网关层、核心服务入口 |
| 请求透传/快速失败 | 部分场景不需要重试,直接告诉客户端“忙不过来”。 | 非关键操作,或负载已很高时 |
我个人最喜欢“指数退避 + 熔断器”组合。指数退避让重试间隔拉开,熔断器在连续失败后直接切断链路。两者加起来,即使下游挂了,上游也不会往死里打。
4.3 归纳对比:幂等 vs 雪崩
有时候新人会混淆这两个概念。说白了,幂等是解决“重试会不会造成数据错乱”,雪崩是解决“重试会不会把系统搞崩”。一个是数据一致性,一个是系统可用性。它们可以独立存在,但往往同时出现。
拿一个场景帮你想透:假设你做了一个短信发送服务,对外提供接口。如果下游短信网关超时,你决定重试。这时候,不解决幂等 → 用户收到多条重复短信。不解决雪崩 → 重试风暴把短信网关打挂,后面所有人的短信都发不出去。两条都得管。
4.4 本章知识体系图
下面这张图把重试可能引发的问题以及对应的解决方案串了起来。你一眼就能看到全局。