用 Python 写一个可复用的重试装饰器:指数退避、随机抖动与幂等边界
调用网络接口、数据库、第三方服务时,偶发失败几乎不可避免:网络抖动、服务限流、连接池一时满了,第一次失败并不等于业务真的失败。这时最常见的错误做法,是在业务代码里到处写 try 和 sleep,或者无脑循环重试。正确的做法是把重试抽成一个装饰器,让业务函数只关心一次调用的语义,把重试策略集中管理。本文给出一个可直接复用的实现,并讲清三个关键点:指数退避、随机抖动和幂等边界。
一、为什么需要指数退避
固定间隔重试有一个典型问题:如果很多客户端同时失败、同时以相同间隔重试,下一轮请求又会同时打到服务端,形成重试风暴。指数退避让等待时间随失败次数增长,例如第一次等 0.5 秒,第二次等 1 秒,第三次等 2 秒,给服务端留出恢复时间,也避免把一次抖动放大成持续过载。实践中还要设上限,例如最多等 10 秒,否则一次调用可能拖垮整个请求的超时预算。
二、为什么要加随机抖动
即使有了指数退避,如果所有客户端的退避序列完全相同,它们仍会在同一时刻醒来。加一点随机抖动可以把重试时刻打散。常用做法是全抖动:实际等待时间在 0 到指数退避上限之间随机取一个值;也可以用等抖动:取退避时间的一半再加一半以内的随机值。抖动不是为了好看,而是为了让重试流量在时间上均匀分布,这对限流场景尤其重要。
三、哪些异常可以重试,哪些不能
重试的前提是失败可能是暂时的。连接超时、连接重置、服务端返回 502、503、504,或者明确的限流响应,通常可以重试。参数错误、鉴权失败、找不到资源、数据校验失败,重试多少次结果都一样,只会浪费时间和配额。装饰器应当允许调用方传入可重试异常类型和可重试状态码,而不是捕获所有异常。把异常分类写进策略,是重试逻辑能否长期不出问题的前提。
四、一个可复用的实现
下面是一个同步函数的版本,核心参数包括最大重试次数、初始延迟、退避倍数、最大延迟和可重试异常:
import random
import time
from functools import wraps
def retry(max_attempts=4, base_delay=0.5, backoff=2.0, max_delay=10.0, retry_on=(TimeoutError, ConnectionError)):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
delay = base_delay
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except retry_on as exc:
if attempt == max_attempts:
raise
sleep_time = random.uniform(0, min(delay, max_delay))
print(f"第 {attempt} 次失败:{exc},{sleep_time:.2f} 秒后重试")
time.sleep(sleep_time)
delay = min(delay * backoff, max_delay)
return wrapper
return decorator
使用时只需要这样写:
@retry(max_attempts=4, base_delay=0.5)
def fetch_user(user_id):
return call_remote_api(user_id)
这样业务代码里不再出现重复的重试循环,策略改动也只需要改装饰器参数。
五、幂等边界:重试前必须先想清楚
重试最大的风险不是多等几秒,而是把一次操作执行了多次。查询类操作天然可以重试;下单、扣款、发短信、创建订单这类写操作,重试前必须确认服务端是否支持幂等键。常见做法是为每次业务请求生成一个幂等键并随请求一起发送,服务端用幂等键去重,重复提交返回第一次的结果而不重复执行。如果下游不支持幂等,重试次数应当保守,或者只在明确未发出请求的超时场景下重试,并在日志里留下可追踪的请求标识。
六、落地时的检查清单
第一,重试总耗时要小于调用方的超时时间,否则重试反而会拖垮上游。第二,记录每次失败的异常类型、重试次数和最终结果,便于区分偶发抖动和持续故障。第三,不要在重试里掩盖错误,最终失败时应当原样抛出异常,让上游决定降级还是报错。第四,对异步函数不要直接复用 time.sleep,需要用异步版本和 asyncio.sleep。第五,配合熔断使用:当失败率持续很高时,先快速失败而不是继续重试,给系统恢复留出空间。
总结:重试不是越多越好,而是有策略、有边界、有日志。指数退避控制节奏,随机抖动打散流量,异常分类决定能不能重试,幂等键决定写操作敢不敢重试。把这四点写进一个装饰器,业务代码会干净很多,线上偶发失败的恢复能力也会明显更稳。
- 点赞
- 收藏
- 关注作者
评论(0)