你的HTTP重试,是一颗随时会炸的定时炸弹

2026-03-31 11 0

你的HTTP重试,是一颗随时会炸的定时炸弹

先问个问题:你的接口调用失败时,会重试吗?

我相信99%的回答是"会"。那你再想想:重试的时候,有没有考虑过幂等性?有没有区分哪些状态码值得重试?有没有想过万一重试风暴把下游打死了怎么办?

如果你沉默了,恭喜你,今天这篇文章就是为你写的。如果你底气十足,那我建议你还是看看——因为重试这个事儿吧,懂的人越多,生产环境死掉的概率越小。


一、为什么我要专门写重试?

因为这玩意儿看起来太简单了,简单到所有人都觉得自己会,但十个项目里有九个在埋雷。

我见过最离谱的一个案例:某后端哥们儿写了个订单创建接口,调用支付服务失败就无脑重试3次,每次间隔1秒。结果用户点了下单,支付服务其实已经成功扣钱了,但返回的网络包丢了——于是重试了3次,扣了4笔钱。用户拿着4笔扣款记录找上门来,那场面,啧啧。

这就是重试最本质的问题:网络错误不等于业务失败。你以为调用失败了,其实可能只是响应在路上丢了。你重试了,下游以为你发了4次请求。


二、重试的第一原则:幂等性

写重试代码之前,你先问自己一个问题:这个请求重跑一次,结果还是一样的吗?

如果答案是"不一定"甚至"不是",那你的重试就是在玩火。

先说哪些天然幂等:GET、HEAD、PUT、DELETE这些HTTP方法,理论上都可以安全重试。POST和PATCH呢?不一定。看你的业务逻辑。

举个例子:扣款接口,扣100块,重试两次就扣300块,这不幂等。但如果你在请求里带一个客户端生成的唯一trace_id,服务端在数据库里先查一下"这个trace_id处理过了吗",处理过就直接返回之前的结果——这就幂等了。

实现幂等的方式主要有几种:

第一种,唯一键约束。在业务表里加一列client_id,每次请求带上,服务端insert的时候用replace或者on duplicate key——数据库帮你挡重试。第二种,分布式锁。用redis存一个key,过期时间设为业务超时时间,加锁成功才执行,加锁失败说明有别的请求在处理了。第三种,状态机流转。把业务状态分成pending/processing/completed/failed,只有pending状态才能流转到processing,这样重复请求进来直接查状态就能判断。

我的建议是:如果你做不好幂等,就不要重试。宁可丢一条消息,也比多扣一笔钱强。业务丢了可以补偿,钱扣多了那是真金白银的纠纷。


三、状态码判断:你可能一直在瞎重试

很多人写重试是这样的:

try {
    doRequest();
} catch (Exception e) {
    // 全部重试!
    retry();
}

我看到这种代码血压就上来了。不是所有的错误都值得重试

先说HTTP状态码,这几个你必须分清楚:

4xx系列:400 Bad Request,说明你请求本身有问题,你重试一万次还是400,没意义。401 Unauthorized,需要认证,你得先去拿token,拿了token再重试,而不是直接重试。403 Forbidden,权限不足,重试也没用。404 Not Found,资源不存在,重试它还是不存在。429 Too Many Requests,触发限流了,这时候你更应该停下来等一会儿,而不是疯狂重试让它更堵。

那5xx呢?500 Internal Server Error,服务器出问题了,可能重试有用,但也可能服务端已经写了一半数据进去了,你重试可能造成重复写入。502/503/504,网关或下游服务不可用,重试是有意义的,但要配合熔断和退避策略。504超时,有时候服务端其实处理成功了只是响应超时了,这种就涉及幂等性问题。

网络层的错误呢?Connection refused,下游服务没启动,重试也没用。Connection reset,对端崩溃了,重试可能有用。DNS解析失败,Check your network configuration,重试大概率也没用。Read timeout/socket timeout,这个要看你读的是啥数据,如果是读操作超时,重试没问题。

所以一个正确的重试策略,应该是先判断错误类型,再决定要不要重试,以及重试几次。不是catch一个Exception就完事了。


四、重试风暴:最容易被忽视的灾难

假设你有100个并发请求同时打一个接口,这个接口刚好出了点问题,响应变慢了,最终超时了。

如果没有重试控制,这100个请求会同时触发重试,100变成300。如果你的上游还有服务在做聚合调用,一个故障可能会被级联放大,最终把整个系统打挂。这就是重试风暴,也叫惊群效应。

解决这个问题,有几个思路:

第一,指数退避而不是固定间隔。第一次等1秒,第二次等2秒,第三次等4秒,而不是每次都等1秒。这样能有效分散重试请求的时间分布。

第二,设置全局重试上限。不是每个请求都重试3次,而是整个系统的重试QPS有一个上限。超过上限的请求直接失败,不重试。这叫重试限流

第三,熔断器模式。当下游错误率超过某个阈值,直接熔断,后续请求不再调用下游,直接返回降级结果。熔断一段时间后,放几个请求过去试试,如果下游恢复了就关闭熔断,否则继续熔断。

说实话,这三个机制写起来都不复杂,但很多团队就是懒得写。直到某天半夜被报警叫起来处理故障,才后悔莫及。


五、你的重试框架选对了吗?

现在很多团队用Spring Retry、Guava Retry或者自己写重试逻辑。我见过自己写的,while循环加Thread.sleep,没有上限,没有状态码判断,没有退避策略,简直是生产级定时炸弹。

如果你用Java,Spring Retry配合@Retryable注解已经能覆盖大多数场景了,加上exponentialBackoff和retryFor表达式,基本够用。如果你用Go,go-retryablehttp或者自己基于context写一个也不算难,但要注意context取消传播的问题。如果你用Python,tenacity库是个不错的选择,支持指数退避、条件重试、重试次数限制。

选框架不是最重要的,最重要的是你想清楚上面说的这些问题了没有。框架只是帮你少写点代码,但帮你做不了决策。


写在最后

重试这事儿,说难不难,说简单也不简单。难就难在它涉及幂等性、状态码判断、退避策略、限流熔断一堆知识点,每个点没想清楚都可能踩坑。简单就简单在,这些知识点都是可以提前学习、提前设计的。

我见过太多团队是先写业务代码,功能跑通了,测试也过了,然后某天线上出了个奇怪的bug,才发现"哦原来这个接口被重试了这么多次"。

重试应该是设计出来的,不是加上去的。在你写接口之前,就应该想清楚:这个接口能不能重试?重试的时候会不会有副作用?用什么策略控制重试频率?这些问题想清楚了再动手代码,你会少踩很多坑。

当然,如果你现在线上代码还没改,那就先把这篇文章收藏了,回头有空的时候review一下自己的重试逻辑——看看是不是埋着雷。

希望这篇文章没白写,希望你的生产环境别出事。出了问题也别来找我,找我我也不会修。开玩笑的,会修的。收工。

相关文章

不想折腾了?让小龙虾帮你一键部署 AI 工具!🦞
不想折腾了?让小龙虾帮你一键部署 AI 工具!🦞
为什么你的API总是被吐槽?我总结了7个血泪教训
你的API正在悄悄谋杀你的数据库——而你可能毫不知情
还在为部署AI工具熬夜?小龙虾帮你躺平!🦞
还在为部署AI工具熬夜?小龙虾帮你躺平!🦞

发布评论