ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Python重试库对比:自制300行代码vs Tenacity 12行实现

2026/8/30 19:29:40 拓冰建站 浏览量
Python重试库对比:自制300行代码vs Tenacity 12行实现 一、写代码的陷阱很多程序员都在盲目重复造轮子相当多的开发者都有着如此这般的经历, 自己所编写的代码模块乍一看颇为精巧, 然而实际上却暗暗地埋下了一大堆的技术债务情形。有开发者踏入了一个经典的坑淖之中: 本以为要用十几行代码便能够将其搞定的功能内容, 但其却是强行地书写出了三百行堪称维护噩梦的代码情况。有一位开发者, 他在从事多第三方 API 同 步业务期间, 外部接口存在各类不稳定状况, 流量高峰时会抛出503报错, 接口限流会返回429, 数据传输途中出现 TCP 连接断开。该业务需要一套具备高可靠性的重试逻辑, 他没有选用现成工具, 而是选择自己手写一套重试引擎。刚开始仅是单纯进行try加while循环, 因生产环境要求持续增多, 代码编写量逐渐增大。需达成指数退避, 随机抖动避免惊群效应, 区分可重试异常与致命错误, 日志记录, 同步异步兼容, 众多需求纷至沓来, 最终产出自编的三百行带重试功能的代码。此套代码于本地测试时, 看上去所有方面均正常, 然而, 当投入到真实业务之中, 在与异步爬虫、定时任务、接口服务进行对接之后, 各种各样的边界问题便接二连三地爆发出来。至于异步函数, 需要将整套类重新进行复制并再次编写, 无法依据返回的结果来进行重试操作, 若要增加超时终止条件, 又得额外新增加几十行用于状态管理的代码。最终, 三百行代码演变成了难以进行维护的技术债务了。项目有着这样的介绍, 其开源协议是为了那个特定的.0, 它呈现的是完全免费且开源的状态, 它可是所处生态专门致力于做出重试处理方面的库, 它拥有着超过6000星的关注度, 它被广泛运用在数量众多的企业级项目当中, 它专门针对接口调用、网络请求场景去解决重试逻辑开发的问题。不少程序员会陷入这样的思维, 即觉得这点逻辑蛮简单, 自己去写会更快。自己打造轮子的确能加深对底层原理的理解, 这方面值得予以肯定。然而辩证地去看, 生产环境下的代码可不光是要实现功能, 还得去处理数量众多的边界场景, 要兼容同步以及异步情况, 还要把异常堆栈保留妥善。手写代码看似是完成了需求, 可隐藏着的 bug 会在流量上升之后集中地爆发出来。提供给诸位思想的空间: 你可曾针对一项单纯功能亲手编写了一长串工具代码, 后续维护之际痛苦不堪?二、核心进行拆解, 展现三百行自己编写的代码与, 十几行达成相同能力所进行的、原始实现的共计三百行需自己再编重来制成进行比较。开发者花费了三天的时间, 进行编写后调试, 完整地达成了指数退避, 随机抖动, 异常过滤, 日志告警, 接下来是核心实现代码。import time import random import logging import functools from typing import Callable, Any, Tuple, Type logger logging.getLogger(retry_engine) class CustomRetryEngine: def __init__( self, max_attempts: int 5, base_delay: float 1.0, max_delay: float 60.0, factor: float 2.0, jitter: bool True, retryable_exceptions: Tuple[Type[Exception], ...] (Exception,), fatal_exceptions: Tuple[Type[Exception], ...] () ): self.max_attempts max_attempts self.base_delay base_delay self.max_delay max_delay self.factor factor self.jitter jitter self.retryable_exceptions retryable_exceptions self.fatal_exceptions fatal_exceptions def _calculate_delay(self, attempt: int) - float: # 指数退避计算公式 delay self.base_delay * (self.factor ** (attempt - 1)) delay min(delay, self.max_delay) # 随机抖动避免惊群效应 if self.jitter: delay random.uniform(0, delay) return delay def __call__(self, func: Callable[..., Any]) - Callable[..., Any]: functools.wraps(func) def wrapper(*args: Any, **kwargs: Any) - Any: attempt 1 while attempt self.max_attempts: try: return func(*args, **kwargs) except self.fatal_exceptions as fatal_err: logger.error(fFatal error encountered in {func.__name__}: {fatal_err}. Not retrying.) raise fatal_err except self.retryable_exceptions as err: if attempt self.max_attempts: logger.critical(fMax retry attempts ({self.max_attempts}) reached for {func.__name__}. Failing.) raise err sleep_time self._calculate_delay(attempt) logger.warning( fAttempt {attempt}/{self.max_attempts} failed for {func.__name__} due to {err}. fRetrying in {sleep_time:.2f}s... ) time.sleep(sleep_time) attempt 1 return None return wrapper这套代码具备完成基础重试的能力, 然而其短板极为显著, 具体表现为: 对异步函数缺乏原生支持, 无法依据接口返回结果来触发重试操作, 若要新增总超时终止条件则需进行额外开发工作, 并且大量边界情况都得由开发者自行去处理。2.2 实现同等业务逻辑同样的需求, 指数退避, 随机抖动, 只捕获临时网络异常, 日志告警, 只需要少量声明式配置, 便能够完成。import logging import requests from tenacity import ( retry, stop_after_attempt, wait_random_exponential, retry_if_exception_type, before_sleep_log ) logger logging.getLogger(__name__) class TransientNetworkError(Exception): pass retry( stopstop_after_attempt(5), waitwait_random_exponential(multiplier1, max60), retryretry_if_exception_type(TransientNetworkError), before_sleepbefore_sleep_log(logger, logging.WARN), reraiseTrue ) def fetch_user_data(user_id: int): response requests.get(fhttps://api.example.com/users/{user_id}, timeout5) if response.status_code in (500, 502, 503, 504): raise TransientNetworkError(fServer error: {response.status_code}) elif response.status_code 429: raise TransientNetworkError(Rate limited.) return response.json()皆采用声明式装饰器。不用手写循环。无需延时计算逻辑。所有复杂底层逻辑库于内部已然封装完成。一个小的知识点是, 要是不加随机抖动的话, 就会产生惊群效应, 在出现上千个任务同时失败的情况之后, 以固定间隔同时发起重试请求, 这样会直接把服务打垮, 还有呀, 随机延时能够打散请求时间, 可以规避上述的那种风险。2.有三个大部分开发者都不知道的高级用法, 用法 1 是, 依据返回结果触发重试, 而不是通过抛出异常来处理。不少接口并不会抛出异常, 只是返回异常状态码, 能够直接依据返回值来进行重试。from tenacity import retry, retry_if_result import requests def is_rate_limited(response): return response.status_code 429 retry(retryretry_if_result(is_rate_limited)) def query_payment_gateway(): return requests.get(https://api.stripe.com/v1/charges)用法 2多终止条件组合能够同步设定最大的重试次数, 还有总的执行超时时间, 一旦满足其中任意一个条件, 便会停止重试。from tenacity import retry, stop_after_attempt, stop_after_delay retry(stop(stop_after_attempt(5) | stop_after_delay(30))) def query_database(): pass用法 3原生支持异步 async 函数不必要去改写任何的包装代码, 能够自动实现识别协程, 进而适配像 httpx 这类的异步请求库。import httpx from tenacity import retry, stop_after_attempt retry(stopstop_after_attempt(3)) async def async_fetch(url: str): async with httpx.AsyncClient() as client: return await client.get(url)起到实际作用的小提示, 装饰器来配备True相当关键。库里默认抛出的是普遍通用的异常, 将这个参数开启的举动, 会抛出原本的异常, 堆栈方面的信息完整无缺, 在网络线上排查问题会更便利。手写属于自己的重试逻辑, 能够用以深化对于底层原理的理解, 这般情况对于技术成长具备相当大的价值。不过从辩证角度予以检视, 线上业务系统应当优先追求稳定以及可靠, 那些已然历经大量项目予以验证的开源库, 走过无数开发者精心研磨, 相比个人手写代码所顾及的场景更为周全。大家能够进行思考: 何种场景适宜自行制造轮子, 怎样的场景直接选用成熟的开源库?三、辩证分析不要极端看待造轮子与直接引用第三方库来到此处, 极易萌生出一个念想, 接下来所有工具统统径直采用开源库, 全然不再自行编写代码, 然而, 这一结论太过武断。先使用成熟的开源库, 其优势明显可见。其一, 能节省大量开发调试的时间, 在案例里直接将三百行自定义代码删掉, 从而减轻维护的负担其二, 可以规避大量隐性的bug, 此项目已在众多企业项目中落地, 各类边界问题都已被修复其三, 同步、异步、多条件组合等高级能力能够直接使用。但我们也绝不能将造轮子的价值一股脑儿地完全否定。在学习时期亲手去书写重试逻辑, 能够把指数退避、随机抖动、异常分类这些属于网络稳定性方面的核心概念彻底琢磨透彻, 这可是相当不错的用于锻炼自身能力的项目。要是业务场景有着极其特殊的情况, 现有的开源库没办法进行适配, 而在极简场景下引入第三方库又会让项目的依赖负担有所增加, 处在这种时候进行轻量的自主研发同样是存在一定合理性的。真正应当予以警惕的, 乃是生产环境之中盲目地进行自我研发, 好多开发者对自身估计过高, 认为逻辑简易, 写出来之后仅仅测试正常流程, 却忽略各种各样的异常边界情况, 等到上线之后, 在并发以及网络出现抖动的场景之下, 隐性的bug集中性地爆发出来, 后期修复的时候成本极其高昂。辩证地进行观察, 优秀工程师所具备的能力, 并非在于能够书写出数量众多的代码行数, 而是在于其拥有敢于将无效且冗余代码予以删除的勇气与决断。以此来看, 通过代码编写从而实现需求固然是一项基础性质的能力。进而, 懂得对经过验证可行的工具加以复用, 同时狠心砍掉诸如那些不必要的自主研发代码, 这才是迈向更高层级的工程思维特质体现。大伙能够思索一番, 在你的项目之中, 究竟存有多少行数的代码, 它原本是能够借助开源库来予以替换的呢?四、现实意义很多技术债务源于盲目重复造轮子此案例并非单单只是介绍这么一个库, 它所反映出的是众多开发团队广泛存在的一种状况, 是一种情形 , 是一种现状。许多项目仓库之中, 存有大量自行研发的工具脚本, 这些脚本是开发者当初为解决某个小需求而迅速开发完成的, 并未全面覆盖边界场景, 随着业务一代代发展变化, 持续在之上添加修补代码, 代码行数不断增加, 渐渐变成没人敢加以改动的技术债务。和案例情况一样, 在删除了三百行自行研发的代码后, 直接将3个与隐性时间有关的bug给消除了, API同步层整体稳定性就直接变成原来的两倍了。对于那些普通的开发者而言, 这件事情给他们带来了两点具有现实意义的启发, 其一, 在开发业务功能之前, 要先去检索那些成熟的开源组件, 不要一开始就直接动手去写, 而是要优先去看看行业里面已经被验证过的方案, 以此来避免重复地踩坑其二, 要区分学习场景和生产场景, 在学习练习的时候, 可以放心大胆地去造轮子以便吃透原理, 而对于线上生产业务来说, 稳定性是优先考虑的, 要优先选用成熟的方案。那自然也不能够毫无节制地去引入第三方依赖, 对于每一次新增出来的一个依赖包而言, 它都会随之引发版本兼容以及安全漏洞方面的风险。此时就需要去做一番权衡的, 要是等到自家研发的时候居然需要几十行数甚至上百行数的代码, 那么优先选择开源库就是比较合适的可假设通过两三行代码就能够完成搞定那个看似简单的逻辑, 那就直接手写就可以。在工程开发当中, 并非仅仅比拼到底是谁所编写的代码行数更为可观, 首要且关键的目标实则乃具备稳定性、对于维护而言简易可行以及存在极少的漏洞缺陷。五、互动话题阅览完此案例, 给众人留下若干问题, 欢迎于评论区一同交流: 其一, 你是否曾手写一长串工具代码, 上线后各类问题层出不穷? 最终是进行重构还是勉强硬着头皮维护? 其二, 在何种情形下你会倾向选择自行打造轮子, 又在什么情境下直接运用开源库? 其三, 除了既定的某些内容, 你还知晓哪些可替代大量自研逻辑的宝藏开源库? 欢迎予以分享出来。