Python重试装饰器:指数退避、异常分类与可测试性设计
调用外部接口时,偶发的网络抖动、限流和服务端短暂不可用都很常见。一个写得好的重试装饰器,不是简单地把失败调用再跑几遍,而是要先回答三个问题:这次失败值不值得重试、等多久再重试、以及如何证明它真的按预期工作。
一、先分类,再重试
把异常分成两类,是重试设计的第一步。
- 值得重试:连接超时、连接被重置、HTTP 429、502、503、504。这类失败通常是暂时的,稍后重试有成功可能。
- 不值得重试:参数错误、鉴权失败、资源不存在、业务校验失败。重试不会改变结果,只会浪费时间和配额,还可能把一次错误放大成多次重复请求。
对非幂等操作要格外谨慎,例如下单、扣款、发送通知。没有幂等键或去重机制时,盲目重试可能导致重复执行,这类操作宁可直接失败并人工确认,也不要自动重放。
二、指数退避加抖动
固定间隔重试容易在故障恢复瞬间形成请求洪峰。指数退避的做法是:第一次等较短时间,之后每次等待时间翻倍,并设置上限;再叠加随机抖动,把不同客户端的重试时刻错开。
一个常用的等待时间公式是:
等待时间 = min(最大等待时间, 基础等待时间 × 2 ** 重试次数) + 随机抖动
例如基础等待时间为 0.5 秒,最大等待时间为 8 秒,则前几次的等待时间大约是 0.5 秒、1 秒、2 秒、4 秒、8 秒。抖动一般取 0 到 0.5 秒之间的随机值,避免大量客户端在同一时刻重试。
三、一个可复用的实现
下面的装饰器把重试次数、等待策略、可重试异常和回调都做成参数,调用方只需要声明策略,不需要重复写循环:
import functools
import random
import time
def retry(attempts=3, base_delay=0.5, max_delay=8.0,
retry_on=(TimeoutError, ConnectionError),
on_retry=None, sleeper=time.sleep):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(attempts):
try:
return func(*args, **kwargs)
except retry_on as exc:
is_last = attempt == attempts - 1
if is_last:
raise
delay = min(max_delay, base_delay * (2 ** attempt))
delay += random.uniform(0, 0.5)
if on_retry is not None:
on_retry(func.__name__, attempt + 1, delay, exc)
sleeper(delay)
raise RuntimeError("unreachable")
return wrapper
return decorator
使用时可以这样声明:
@retry(attempts=4, retry_on=(TimeoutError, ConnectionError))
def fetch_remote_data():
# 在这里调用外部接口
...
这里有几个值得注意的细节。第一,functools.wraps 保留了原函数的名称和文档,避免调试时看到一堆 wrapper。第二,retry_on 用元组传入,调用方可以按接口特性调整。第三,最后一次失败必须原样抛出异常,不能静默返回空结果,否则上层无法区分成功与失败。
四、让重试可测试
重试逻辑如果直接调用 time.sleep,测试会变慢且难以断言。上面的实现把 sleeper 作为参数注入,测试时传入一个记录调用参数的假函数,就可以在不真实等待的情况下验证行为:
- 失败两次、第三次成功时,函数恰好被调用三次。
- 每次重试之间的等待时间符合指数增长,并且没有超过上限。
- 遇到不在
retry_on中的异常时,立即抛出,不重试。 - 达到最大次数后,最后一次异常被原样抛出。
测试时还可以传入一个记录日志的 on_retry 回调,检查日志里是否包含函数名、重试次数和等待时间。这些断言比只看最终结果更能发现策略写错的问题,例如把次数算错一位,或者把上限写成了固定值。
五、落地前检查清单
- 确认操作是否幂等,非幂等操作先补幂等键或去重逻辑。
- 给整个调用链设置总超时,避免重试把一次慢请求拖成更长的等待。
- 记录每次重试的函数名、次数、等待时间和异常类型,方便事后定位。
- 对 429 限流要格外克制,必要时按服务端返回的等待时间退避,而不是按自己的节奏重试。
- 不要在数据库事务内部无差别重试写操作,先确认失败点是否已经提交。
总结一下:重试不是越多越好。先分类异常,再用指数退避加抖动控制节奏,最后用可注入的等待函数和回调让策略可测试,这样的重试装饰器才能在生产环境里既提高成功率,又不把偶发故障放大成系统性问题。
- 点赞
- 收藏
- 关注作者
评论(0)