news 2026/10/30 23:35:10

重试不是越多越好:退避、抖动和幂等怎么配合

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
重试不是越多越好:退避、抖动和幂等怎么配合

重试不是越多越好:退避、抖动和幂等怎么配合

前几天朋友问我,他的脚本老失败,于是给每个请求都套了三层重试。结果失败是少了点,耗时却翻了好几倍,偶尔还把对方的接口搞得返回来一堆重复数据。他问我,重试到底该怎么写。

说实话,这问题我太熟了,因为我自己也这么干过。重试就几行代码,可要写对,里面门道不少。

为什么会想到重试

网络这层本来就不稳。一次请求没成,未必是对方把你拒了,也许只是这一下丢了个包,也许对方正好在重启,还有可能是你撞上了它的限流窗口。这类碰运气的失败,隔一小会儿再来一次,往往就过去了。

所以重试是给偶发故障用的,不是给铁定复现的错误用的。这个前提想不清楚,后面全白搭。要是错误每次必现,比如参数写错、权限不够、接口压根不存在,那你重试一百遍也还是同一个结果,白耗时间和资源,弄不好对方还觉得你在恶意刷它。

我现在动手之前会先问自己一句:这个错误,等两秒再来一次,结果会不会不一样?会,那就值得重试;不会,那就别绕弯子,回去老老实实改代码。

我见过最多的三种写法

第一种,固定间隔猛冲。重试三次,间隔全设成 0 秒或者 100 毫秒。对方一过载,你的请求就和一堆别人的请求一起涌上去,把它压得更喘不过气,最后一锅端。

第二种,什么错都重试。404 也重试,500 也重试。可 404 是东西没了,重试并不能让它冒出来;真该重试的只是那些临时的、服务端掉链子的错误。

第三种,忘了设上限。有人重试不设次数,配着固定间隔,理论上能跑到地老天荒。线上真出一次问题,线程全卡那儿,整个服务跟着遭殃。

退避与抖动

改法叫退避,也就是每重试一次,等待时间比上一次长一点。最简单的做法是每次翻倍,涨得快,对方过载时你能迅速让路。嫌涨太猛,也可以用线性增长,每次多加固定的一段。

参数我心里有套常用的:基础间隔 1 秒,每次翻倍,单次最长别超过 30 秒,重试 3 到 4 次。这样算下来,总等待大概是十几秒到一分钟,多数偶发故障都能兜住。

光有退避还不够。要是同一批任务在同一秒全失败,退避又是固定倍数,它们就会齐刷刷地在同一刻重试,然后又一齐失败,如此往复。这个场景有个名字,叫重试风暴。

治法是在等待时间上撒一把随机,业内叫抖动。原本要等 8 秒,就让它落在 4 到 8 秒之间的某个随机点。每个任务错开了,就不会全挤在门口。这招几乎不花钱,我却觉得它是重试里最值的一个细节。

退避也不是越久越好。有人一听要退避,直接把间隔设成几十秒,结果一次小抖动就让任务干等半分钟,整体效率掉得厉害。退避的目的是避开那一小段拥堵,不是让你停下来睡大觉。间隔该给多少,得看对方恢复正常大概要多久,实在拿不准,就从小往大调,别一上来就拉满。

哪些错误能重试

把错误分个类,会清楚很多。连接超时、读取超时、连接被重置,这些都值得再来一次;服务端的 5xx,尤其 502、503、504,通常也值得。429 表示请求太多,也能重试,但得看对方给的 Retry-After 头,人家说等多久就等多久,别自作聪明。

那 4xx 呢?除了 408 和 429,剩下的基本别重试。401、403 是身份问题,你再来一遍还是那张脸;404 是没有;400 是请求本身就写错了。这些错误赶紧记下来去查,比你反复撞墙有用。

一句话的判据:这个错误,是不是缓一缓可能会好。是,就重试;不是,就省省。

幂等:最容易被忘的一环

这点我吃过亏,所以特别想强调。重试的意思是,同一个操作可能被执行两遍甚至更多遍。要是这个操作本身不允许重复,麻烦就来了。

所谓幂等,是执行一次和执行多次,最终结果一样。读取、查询这种,天生幂等,随便重试;可下一单、扣一次库存、发一条消息这种带副作用的,重试就可能冒出一份多出来的。

所以写操作要么做成幂等,比如每次带一个唯一的请求号,服务端看到重复号就直接忽略;要么干脆不重试,把失败交给上层去判断。我刚写重试那会儿完全没这根弦,结果一批任务重复提交,事后清理得我头大。

判断也不难。问自己:如果对方其实已经收到了、只是回执在半路丢了,我再发一次,会不会出事?会,那就别盲目重试。

顺便说下幂等号怎么给。简单点的做法,是用一个能唯一标识这次业务的字符串,比如订单号,原样带在请求里;复杂点的,可以拿业务字段拼一个摘要。关键是这个号要对同一次业务保持稳定——你重试的时候得带同一个号,不能每次重新生成,不然服务端看到的就是两个不一样的请求,照样重复处理。

封装起来

散着写 try/except 太乱,策略也不好统一。我一般封个小函数,把退避、抖动、次数、该不该重试的判断都塞进去,业务代码管调用就行。

importtimeimportrandomimportrequestsdefshould_retry(status=None,exc=None):ifexcisnotNone:returnisinstance(exc,(requests.ConnectionError,requests.Timeout))ifstatusisNone:returnFalseifstatus==429orstatus>=500:returnTruereturnstatus==408defrequest_with_retry(method,url,retries=4,base=1.0,cap=30.0,**kwargs):forattemptinrange(retries+1):try:resp=requests.request(method,url,**kwargs)ifresp.status_code<400ornotshould_retry(status=resp.status_code):returnrespifattempt==retries:returnresp delay=min(cap,base*(2**attempt))delay*=(0.5+random.random()*0.5)time.sleep(delay)except(requests.ConnectionError,requests.Timeout):ifattempt==retries:raisedelay=min(cap,base*(2**attempt))delay*=(0.5+random.random()*0.5)time.sleep(delay)

几个地方顺带提一句。退避用 2 的幂,外面套个 cap,别让它无限翻。抖动取半区间到全区间的随机,目的是把大家的节奏打散。重试的触发条件收窄,只认连接错误、超时和那几个状态码,其余原样返回。

另外有个小坑:requests 的超时一定要显式写,不然某些情况下会一直挂着不返回,你的重试逻辑根本没机会跑。这事我踩过,记到现在。

一个让我长记性的例子

说个真事。有回我做一个对账任务,给扣减接口加了重试。跑了没两天,账面上冒出来一批重复扣减,客户找上来我才发现。查了半天,问题就出在那个接口不幂等,而我重试的时候又没带唯一号。那次之后我给自己立了条规矩:凡是要重试的写操作,先问清楚它重复执行会不会出事,问不明白就先别加重试。

后来我给那个接口补了一个业务单号,服务端认单号,重复的请求直接丢。补完之后,同样的问题再没出现过。代价其实很小,就是一个字段,可没这个字段之前,我怎么调重试参数都没用。

重试和超时怎么搭

这两个常常一起出现,也常常一起被配错。超时管的是这一次请求等多久算失败,重试管的是失败之后要不要再试。它们得配合着设,单看哪一个都不对。

我一般的思路是,单次超时别设太长,几秒到十几秒,够用就行;总的等待时间要有个上限,别让一个任务卡在那里几十分钟。重试次数乘以单次超时,再加上退避的等待,不该超过你业务能容忍的最长时间。超出这个时间的失败,宁可让它报错,也别硬拖着。

还有个细节,重试的时候通常会把超时稍微放大一点。第一次 5 秒就断,第二次不妨给到 8 秒,给对方多一点喘息的余地。具体加多少看场景,但把这个念头带上,比全用同一个值更从容。

日志别偷懒

加了重试,排查反而变难,因为一次失败背后可能藏着好几次尝试。我的习惯是每次尝试都记一条,写清楚第几次、耗时多久、为什么败。回头一看就明白,是对方一直不听话,还是我自己的策略太激进。

要是日志里只有干巴巴一句请求失败,那基本等于没记。

最后唠一句

重试的参数网上有现成的,抄一抄很快,难的是知道自己为什么这么设。我见过太多人,把重试当成一块遮羞布,以为加上了就等于稳了,结果把真正的问题盖住,等到量一大才集中爆出来。参数是死的,判断是活的,花点时间想清楚上面那几个问题,比背参数管用得多。

也说句实在的,重试做得再漂亮,也救不了一个本身就不该重试的操作。前面幂等那一节,才是这套东西的地基。地基没打,楼盖得越高越危险。我自己就是先摔了一跤,才回头补的这门课。

小结

重试这东西,目标不是把失败硬盖过去,而是给偶发毛病一次机会,同时别引出新麻烦。退避管节奏,抖动管错开,幂等保安全,日志让你看得见。四样凑齐,才算写得靠谱。

我现在的体会是,真稳的程序不是从不失手,而是摔了能自己爬起来,还不留尾巴。

互动时间:你在项目里重试一般设几次?有没有被重试风暴坑过?评论区聊聊。
免责声明:本文为原创技术分享,涉及的思路仅供学习与合法用途参考。请遵守目标平台的服务条款与相关法律法规,勿将技术用于任何违规场景。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/23 10:58:55

MCP实战:用模型上下文协议构建AI能力组织与编排平台

1. 项目定位与整体设计思路“Rally for AI Rights: Organizing via MCP App”这个项目标题&#xff0c;我曾经在技术社区里看过不下三次&#xff0c;每次都会有人追问&#xff1a;这里的“AI Rights”到底指什么&#xff1f;是给AI平权&#xff1f;是伦理运动&#xff1f;还是某…

作者头像 李华
网站建设 2026/10/18 20:24:35

VS Code AI辅助编程环境搭建:从安装到插件配置的完整指南

1. 为什么值得折腾一套AI辅助编程环境如果你现在还在用记事本或者老旧的编辑器写代码&#xff0c;那真的有点亏。Visual Studio Code&#xff08;后面统一叫VS Code&#xff09;这几年几乎成了编程圈的默认选择&#xff0c;不是因为它花哨&#xff0c;而是它把“轻量”和“能扩…

作者头像 李华
网站建设 2026/10/23 8:45:18

Java+Vue全栈新闻系统开发实战与性能优化

1. 项目概述&#xff1a;全栈新闻资讯系统开发实战去年接手的一个高校融媒体中心项目&#xff0c;让我对新闻资讯类系统的开发有了全新认识。这个基于JavaVueSpringBoot的技术栈组合&#xff0c;不仅完美支撑了日均10万的访问量&#xff0c;其模块化设计还让后期功能扩展变得异…

作者头像 李华
网站建设 2026/10/22 5:39:03

SpringBoot+Vue+MyBatis+MySQL前后端分离IT交流平台实现

前后端分离已经不是什么新鲜概念了&#xff0c;但真正能从头到尾跑通一套“SpringBootVueMyBatisMySQL”完整项目的资料&#xff0c;还是不多。这几年我陆续接过几个IT交流类平台的私活&#xff0c;从课程问答社区到内部技术分享系统&#xff0c;核心形态其实都差不多&#xff…

作者头像 李华
网站建设 2026/10/22 11:38:14

锂电池热失控COMSOL仿真与多物理场耦合建模实战

1. 锂电池热失控仿真&#xff1a;从理论到COMSOL实战作为一名在电池仿真领域摸爬滚打多年的工程师&#xff0c;我深知热失控问题就像悬在锂电池头上的达摩克利斯之剑。今天要分享的这个COMSOL热失控模型&#xff0c;是我经过数十次迭代验证的实战成果&#xff0c;它能完整复现从…

作者头像 李华
网站建设 2026/10/29 23:41:03

AI智能体如何审核路演材料?人机协同下的融资申请新逻辑

1. 这个问题的由来&#xff0c;以及为什么你该认真对待最近我在整理创业融资相关的内容时&#xff0c;频繁被问到同一个问题&#xff1a;某家顶级创业孵化器——就是那个每年收到数万份申请、以“批量录取”闻名的加速器——会不会已经在用AI智能体来审核路演材料了&#xff1f…

作者头像 李华