结论:优先使用 tenacity 库替代手写 while 重试——它更现代、可控,原生支持异步;手动实现易遗漏指数退避、jitter、异常筛选、边界控制等关键逻辑,且难以维护。

直接说结论:别手写 while 循环重试,用 tenacity 库——它比 retrying 更现代、更可控,且原生支持异步。
为什么不用自己写 while + try/except?
手动实现容易漏掉关键逻辑:比如指数退避(避免雪崩式重试)、 jitter(防同步风暴)、重试条件判断(不是所有异常都该重试)、最大重试次数边界控制。更麻烦的是,一旦加了日志、超时嵌套、异步支持,代码迅速变得难读难维护。
常见错误现象包括:ConnectionError 重试了但没等间隔直接冲垮下游;429 Too Many Requests 没解析 Retry-After 头就盲目重试;或者把 ValueError 这类编程错误也纳入重试,导致问题被掩盖。
实操建议:
立即学习“Python免费学习笔记(深入)”;
- 只对网络类异常(如
requests.exceptions.ConnectionError、Timeout)和明确的 HTTP 状态码(如502、503、504)重试 - 跳过客户端错误(
4xx,除429外)和数据解析异常(json.JSONDecodeError) - 首次重试至少等待 1 秒,后续用指数退避(如 1s → 2s → 4s),并加入随机 jitter(±0.5s)
用 tenacity 重试 requests 请求(同步场景)
tenacity 的装饰器语法清晰,条件可组合,且默认不捕获所有异常——这点比很多轮子更安全。
示例:重试 3 次,仅针对连接失败、超时、以及 HTTP 5xx 响应:
图片提示词生成器?不止如此。 马甲系统 —— 把脑海中的画面,翻译成AI能理解的专业表达。 用得越多,它越懂你:首次需要多问几句确认方向,用久了几乎一说就懂。 用得越多,它越快:缓存机制让后续对话越来越省。 RAG进化:成功案例持续入库,越跑越聪明。 输入「新手指南」查看完整功能介绍
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type, retry_if_result import requests <p>def is_server_error(response): return response is not None and 500 <= response.status_code < 600</p><p>@retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=10), retry=( retry_if_exception_type(requests.exceptions.ConnectionError) | retry_if_exception_type(requests.exceptions.Timeout) | retry_if_result(is_server_error) ), ) def fetch_data(url): resp = requests.get(url, timeout=5) resp.raise_for_status() # 触发 4xx/5xx 异常 return resp.json()
注意点:
-
wait_exponential的min和max单位是秒,别设成毫秒(会立刻重试) -
retry_if_result是在函数返回后判断,适合检查响应体或状态码;而retry_if_exception_type是在抛异常时触发 - 如果 API 返回
429并带Retry-After,得自己写一个retry_if_result提取 header 并动态设置等待时间(tenacity不自动解析)
异步请求(aiohttp)怎么重试?
tenacity 原生支持 async def 函数,但必须用 @retry 装饰器 + await 调用,不能混用同步/异步装饰器。
关键差异:
- 不能对
aiohttp.ClientSession.get()直接装饰,要封装成独立的async def函数 - 异常类型要用
aiohttp.ClientError和asyncio.TimeoutError - 重试间隔仍用
wait_exponential,但内部会自动await asyncio.sleep()
示例片段:
import aiohttp from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type <p>@retry( stop=stop_after_attempt(3), wait=wait_exponential(min=1, max=10), retry=retry_if_exception_type((aiohttp.ClientError, asyncio.TimeoutError)), ) async def fetch_async(url): async with aiohttp.ClientSession() as session: async with session.get(url, timeout=aiohttp.ClientTimeout(total=5)) as resp: resp.raise_for_status() return await resp.json()
性能影响:每次重试都会新建 ClientSession(上例中),实际应复用 session。所以真实项目里,session 要作为参数传入,或通过依赖注入管理。
重试逻辑藏在哪?最容易被忽略的三个点
重试不是加个装饰器就完事。真正上线后出问题,往往卡在这几个地方:
- 没统一处理重试日志——建议在
before和after回调里打日志,记录重试次数、异常类型、当前等待秒数 - 没隔离重试上下文——比如一次批量请求中某条失败,重试它不该影响其他请求的 timeout 计时,需确保每次重试是独立 call
- 没考虑幂等性——GET 重试安全,但 POST/PUT 若非幂等接口,重复提交可能产生脏数据;这时重试前必须确认接口是否支持重试语义
复杂点在于:真实 API 的重试策略往往是混合的——有些要按 Retry-After 等,有些要固定间隔,有些还要降级到备用域名。这时候得用 tenacity 的 retry 组合子 + 自定义谓词,而不是硬编码一堆 if。

















