用 Python 写一个带指数退避的重试装饰器:从超时到幂等的完整实践
调用第三方接口时,偶发的超时和 503 并不意味着业务失败。直接让请求失败会把瞬时抖动放大成用户可见的错误;无限制重试又可能把下游打垮。本文用一个 Python 装饰器把重试做完整:只对可重试异常重试、指数退避加随机抖动、设置总次数与超时上限,并把幂等键的思路讲清楚。
先区分两类失败
可重试:网络超时、连接重置、429、502、503,通常是瞬时的。对这类错误,稍后重试有较高成功率。
不可重试:400 参数错误、401 鉴权失败、404 资源不存在、业务校验失败。重试只会重复消耗资源,应当立即返回错误。
装饰器的做法是维护一个白名单,例如 TimeoutError、ConnectionError 以及自定义的 RetryableError,其他异常直接抛出。
指数退避加抖动
固定间隔重试容易让大量客户端在同一时刻再次涌向故障服务,形成重试风暴。指数退避让等待时间按 0.5 秒、1 秒、2 秒、4 秒增长,并叠加随机抖动打散时间点。
import random
import time
from functools import wraps
def retry(max_attempts=4, base_delay=0.5, max_delay=8.0):
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 (TimeoutError, ConnectionError) as exc:
if attempt == max_attempts:
raise
sleep = min(delay, max_delay) + random.uniform(0, 0.3)
time.sleep(sleep)
delay *= 2
return None
return wrapper
return decorator
重试必须配合幂等
如果第一次请求其实已经在服务端执行成功,只是响应丢失,重试会造成重复下单或重复扣款。写操作应当携带幂等键,例如订单号或客户端生成的 UUID,服务端用该键去重:第一次执行并记录结果,后续相同键直接返回第一次的结果,不再重复执行业务。
几个容易踩的坑
第一,不要在重试里吞掉异常日志。每次失败都应记录尝试次数、异常类型和下一次等待时间,否则线上排障时只能看到最后一次失败。
第二,不要把重试次数设得过大。重试总耗时应当有上限,建议结合接口超时一起评估,例如单次超时 2 秒、最多 4 次,总耗时控制在可接受范围内。
第三,批量任务里不要串行死等。可以在队列层面做延迟重试,让失败任务先回到队列尾部,释放当前工作线程。
小结
一个可用的重试策略是:只重试瞬时错误、指数退避加抖动、限制总次数与总耗时、写操作必须幂等。把这四点写进装饰器和接口约定里,系统的抖动容忍度会明显提高,而不会把一次短暂故障演变成雪崩。
- 点赞
- 收藏
- 关注作者
评论(0)