直接用 time.sleep() 做重试会导致并发请求共振雪崩,必须用指数退避加随机抖动(jitter)错开重试时间;仅对连接错误、超时和5xx服务端错误重试,4xx和解析异常等客户端错误不应重试;默认最多3次,超时与幂等性须严格保障。

为什么直接用 time.sleep() 做重试会出问题
很多同学第一次写重试逻辑,就是捕获异常后 time.sleep(1) 再循环,结果遇到网络抖动时请求全堆在第2秒涌上去,把下游打挂。这不是重试,是雪崩。退避策略的核心不是“等一会儿”,而是“错开重试时间、降低并发冲击”。固定间隔重试(fixed backoff)只适合极低频场景;真实API调用必须用指数退避(exponential backoff),并叠加随机抖动(jitter)防共振。
-
requests.exceptions.ConnectionError、requests.exceptions.Timeout、HTTP 5xx 状态码才值得重试;4xx(比如 400、404)基本是客户端错,重试没意义 - 不要对所有异常无差别重试——比如
JSONDecodeError是响应体解析失败,大概率是服务端返回了 HTML 错误页,再试十次还是错 - 默认最多重试 3 次(含首次),超过这个数就该让上游感知失败,而不是静默吞掉
用 tenacity 库实现带 jitter 的指数退避
手写 while 循环 + sleep 容易漏状态清理、超时控制和日志埋点。tenacity 是目前最稳的 Python 重试库,原生支持 jitter、stop 条件、retry-if 判断,且线程安全。
安装:
pip install tenacity
典型用法:
立即学习“Python免费学习笔记(深入)”;
from tenacity import retry, stop_after_attempt, wait_exponential_jitter, retry_if_exception_type
import requests
<p>@retry(
stop=stop_after_attempt(3),
wait=wait_exponential_jitter(max=8), # base=1s, max=8s, 自动加随机抖动
retry=retry_if_exception_type((
requests.exceptions.ConnectionError,
requests.exceptions.Timeout,
))
)
def call_api(url):
resp = requests.get(url, timeout=5)
if resp.status_code >= 500:
raise requests.exceptions.HTTPError(f"Server error: {resp.status_code}")
return resp.json()</p>-
wait_exponential_jitter默认 base=1,即第1次重试等 ~1–2s,第2次 ~2–4s,第3次 ~4–8s;max=8限制上限,避免退避过长 - 别忽略
timeout参数:requests.get(..., timeout=5)是总超时,不是连接超时;若需更细粒度,用(connect=3, read=5) - 函数内部抛出的异常必须匹配
retry_if_exception_type列表,否则不会触发重试——比如 HTTP 5xx 要自己显式raise
不用第三方库时的手动实现要点
如果项目禁用外部依赖,可用标准库实现最小可行重试。关键是:退避时间必须随次数增长,且每次加随机偏移。
import time
import random
import requests
<p>def call_api_fallback(url, max_retries=3):
for i in range(max_retries):
try:
resp = requests.get(url, timeout=5)
if 500 <= resp.status_code < 600:
raise requests.exceptions.HTTPError(f"5xx: {resp.status_code}")
return resp.json()
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout, requests.exceptions.HTTPError) as e:
if i == max_retries - 1:
raise e</p><h1>指数退避 + jitter:2^i * 1s,再加 0–1s 随机</h1><pre class='brush:python;toolbar:false;'> sleep_time = min(2 ** i + random.random(), 8)
time.sleep(sleep_time)- 用
min(..., 8)代替裸2 ** i,防止第4次重试等 16 秒——实际中极少需要等那么久 -
random.random()提供 0–1s 抖动,比固定值更抗并发高峰;别用random.uniform(0, 1),它和random.random()效果一样但多敲字 - 注意
except分组:把不重试的异常(如JSONDecodeError)放在最后或单独处理,避免被兜底捕获
重试时容易被忽略的副作用
重试不是原子操作,它会放大副作用。比如 POST 接口重复提交、文件上传重复写磁盘、或幂等性没做好的扣款接口被调三次。
- GET 请求天然幂等,可放心重试;POST/PUT/PATCH 必须确认服务端是否支持幂等(看文档有没有
X-Idempotency-Key头) - 如果调用方要自己保幂等,得在重试前生成唯一
idempotency_key并透传,且重试时复用同一个 key,不能每次 new 一个 - 日志里务必打印重试次数(如
attempt=1/3),否则线上出问题时分不清是首次失败还是重试失败 - 监控指标要拆开:总请求数、重试请求数、重试成功率——光看“接口成功率99%”没用,可能是 50% 请求走了 3 次才成功
重试逻辑越简单越好,但退避参数、异常筛选、幂等保障这三点,少一个都会在线上变成隐性故障。


















