1. 为什么我们需要更聪明的重试?
写代码,尤其是和网络请求、数据库交互或者自动化测试打交道的时候,最头疼的莫过于“不稳定”。你精心写好的脚本,跑着跑着,突然因为一个临时的网络抖动、一个页面元素加载慢了半拍,或者一个远程服务响应超时,就“啪”地一下报错退出了。这种偶发性的失败,往往不是代码逻辑问题,而是外部环境的不确定性造成的。
最原始的办法是什么?try...except 一把梭,然后在 except 里写个 while 循环,失败了就睡几秒再试。我早期也这么干过,结果就是代码里到处都是重复的、丑陋的重试逻辑,维护起来简直是噩梦。后来发现了 retry 这个库,感觉像是找到了救星。它用装饰器的形式,优雅地把重试逻辑从业务代码里剥离出来,让代码瞬间清爽了不少。
但是,用久了你会发现,retry 库自带的“基于次数”的重试策略,有时候不够用。比如,你调用一个第三方 API,对方服务可能正在重启,你希望“在接下来的 2 分钟内,只要失败就不断重试,直到成功或者 2 分钟耗尽”,而不是简单地“重试 5 次”。又或者,你希望结合超时控制和重试,比如“每次请求的超时时间是 10 秒,如果超时了就重试,但总的尝试时间不能超过 1 分钟”。这些场景,就需要我们对 retry 库进行深度定制,特别是实现基于超时时间的重试控制。
这篇文章,我就结合自己踩过的坑和实战经验,带你从 retry 库的基础用法开始,一步步深入到如何定制一个功能强大、灵活的超时重试装饰器。你会发现,把重试玩明白了,你的程序健壮性会提升好几个等级。
2. 快速上手:retry库的核心玩法
在开始深度定制前,我们得先把 retry 这个工具本身摸透。它的设计非常简洁,核心就是一个装饰器函数 @retry(),通过不同的参数来控制重试行为。
2.1 安装与最简使用
安装没任何难度,一条命令搞定:
pip install retry
最基本的用法,就是给一个可能会失败的函数加上 @retry() 装饰器:
from retry import retry
@retry()
def connect_to_service():
# 模拟一个不稳定的连接操作
import random
if random.random() < 0.7: # 70%的概率失败
raise ConnectionError("Service unavailable")
return "Connected!"
# 调用
result = connect_to_service()
print(result)
这段代码会无限重试(默认 tries=-1),直到函数成功返回。对于需要“死等”直到成功的场景(比如等待某个关键服务启动),这很有用。但现实中,我们通常需要更精细的控制。
2.2 参数详解:如何配置你的重试策略
retry 装饰器的威力全在它的参数上。我们来拆解一下每个参数的含义和实战效果。
exceptions:抓准你想重试的错误
这是最重要的参数。默认是 Exception,意味着捕捉所有异常并重试。但这很危险,比如 KeyboardInterrupt(用户按了Ctrl+C)也会被重试,导致程序无法正常退出。最佳实践是明确指定你需要重试的异常类型。
@retry(exceptions=(ConnectionError, TimeoutError), tries=3)
def fetch_data():
# 只对连接错误和超时错误进行重试
...
你可以传一个异常类,或者一个异常类的元组。
tries 与 delay:次数与等待的基础
tries:最大重试次数。注意,tries=3表示最多尝试 4 次(初始1次 + 重试3次)。设为-1则无限重试。delay:每次重试前的初始等待时间(秒)。delay=2表示第一次重试前等2秒。
backoff 与 max_delay:让等待时间“聪明”起来
这是避免“惊群效应”的关键。如果所有失败请求都立即、等间隔地重试,可能会对故障服务造成二次冲击。
backoff:退避系数。设置backoff=2,结合delay=1,那么重试间隔将是:1秒,2秒,4秒,8秒……呈指数增长。这给了下游服务更多的恢复时间。max_delay:最大等待间隔。防止backoff导致等待时间过长(比如等到1024秒)。设置max_delay=10,那么间隔增长到10秒后就不再增加。
jitter:加入随机性,进一步分散压力
这是高级玩法。jitter 会在计算好的间隔上,增加一个随机抖动。可以是一个固定值,也可以是一个范围。
jitter=1:在间隔上增加0到1秒的随机时间。jitter=(0.5, 2.5):在间隔上增加0.5到2.5秒的随机时间。 这能确保大量同时失败的客户端不会在同一时刻再次发起请求,完美解决“重试风暴”。
logger:记录重试过程,方便排查
默认会使用 retry.logging_logger 来记录每次重试的日志(级别为 WARNING)。你可以传入自己的 logger 对象,或者设为 None 来关闭日志。
下面是一个综合示例,模拟一个调用外部API的函数:
import logging
from retry import retry
import requests
import random
# 设置一个自定义logger
my_logger = logging.getLogger(__name__)
@retry(
exceptions=(requests.exceptions.ConnectionError, requests.exceptions.Timeout),
tries=5,
delay=1,
backoff=2,
max_delay=30,
jitter=(0.5, 3),
logger=my_logger
)
def call_unstable_api(url):
"""模拟调用一个不稳定的API,遇到连接/超时错误时,使用指数退避+随机抖动进行重试。"""
# 模拟30%的失败率
if random.random() < 0.3:
raise requests.exceptions.ConnectionError("Simulated connection drop")
response = requests.get(url, timeout=5)
response.raise_for_status() # 非200状态码会抛出HTTPError,这个异常我们并未捕获,所以不会重试,会直接抛出。
return response.json()
这个配置的意思是:最多尝试6次(1+5),首次失败等1秒,之后按2的指数退避(2秒、4秒、8秒…),但最多等30秒,并且每次等待加上0.5到3秒的随机抖动。只对 ConnectionError 和 Timeout 进行重试。所有重试行为都会被记录到 my_logger 中。
3. 突破限制:动手实现超时重试装饰器
retry 库很强大,但它有一个根本性限制:它的重试决策只依赖于“次数”(tries),不感知“时间”。而在很多分布式系统和微服务调用中,基于时间的 SLA(服务等级协议) 比基于次数的重试更有意义。
场景假设:你需要调用一个支付网关的查询接口。该接口的 SLA 是 95% 的请求应在 2 秒内返回。你的服务设计是:单次请求超时设为 5 秒,但整个查询操作(允许重试)必须在 30 秒 内完成,无论中间重试了多少次。如果30秒后还没成功,就彻底失败并向上层报告。
这个需求,原生的 @retry 无法满足。我们需要自己造轮子。
3.1 核心思路:时间戳比对循环
实现超时重试的核心逻辑其实不复杂:
- 在函数开始执行时,记录一个“截止时间点”(
deadline)。 - 在一个
while循环中执行被装饰的函数。 - 如果执行成功,直接返回结果。
- 如果抛出异常(且是我们指定要重试的异常),检查当前时间是否已超过
deadline。 - 如果没超时,则等待一段时间(可以加入退避、抖动策略)后,继续循环。
- 如果已超时,则抛出异常,终止重试。
下面是一个基础版本的实现,我尽量让它保持和 retry 库类似的接口风格:
import time
import logging
from functools import wraps
from typing import Tuple, Type, Union, Optional, Callable
import random
def retry_with_timeout(
exceptions: Union[Type[Exception], Tuple[Type[Exception], ...]] = Exception,
timeout: float = 30.0,
delay: float = 0,
backoff: float = 1,
max_delay: Optional[float] = None,
jitter: Union[float, Tuple[float, float]] = 0,
logger: Optional[logging.Logger] = None
):
"""
一个支持超时限制的重试装饰器。
:param exceptions: 需要捕获并重试的异常类型。
:param timeout: 总超时时间(秒)。从第一次调用开始计时,超过此时间则停止重试并抛出最后遇到的异常。
:param delay: 初始延迟时间(秒)。
:param backoff: 退避乘数。每次重试延迟 = 上次延迟 * backoff。
:param max_delay: 最大单次延迟时间(秒)。限制backoff增长的上限。
:param jitter: 随机抖动。可以是固定值(秒)或一个(最小值, 最大值)的元组,会加到每次计算的延迟上。
:param logger: 用于记录重试日志的logger对象。如果为None,则使用print。
"""
if logger is None:
# 简单模拟一个logger
logger = logging.getLogger(__name__)
if not logger.handlers:
logging.basicConfig(level=logging.WARNING)
def decorator(func: Callable):
@wraps(func)
def wrapper(*args, **kwargs):
end_time = time.time() + timeout
current_delay = delay
attempt = 0
while True:
attempt += 1
try:
# 尝试执行函数
return func(*args, **kwargs)
except exceptions as e:
now = time.time()
# 检查是否超时
if now >= end_time:
logger.error(f"[{func.__name__}] 在 {timeout} 秒超时后仍失败,已尝试 {attempt} 次。最后错误: {e}")
raise e # 超时后,抛出最后一次捕获的异常
# 计算下一次等待时间
wait_time = current_delay
# 添加抖动
if isinstance(jitter, tuple):
wait_time += random.uniform(*jitter)
else:
wait_time += jitter
# 确保等待时间不为负数
wait_time = max(0, wait_time)
# 如果设置了max_delay,则进行限制
if max_delay is not None:
wait_time = min(wait_time, max_delay)
# 计算实际能等待的时间(不能超过总超时时间)
remaining = end_time - now
actual_wait = min(wait_time, remaining)
if actual_wait > 0:
logger.warning(f"[{func.__name__}] 第 {attempt} 次尝试失败,错误: {e}。等待 {actual_wait:.2f} 秒后重试... (剩余超时: {remaining:.2f}秒)")
time.sleep(actual_wait)
else:
# 没有时间等待了,直接进入下一次循环(实际上会立刻因超时而退出)
logger.warning(f"[{func.__name__}] 第 {attempt} 次尝试失败,错误: {e}。无等待时间,立即重试...")
# 为下一次重试更新延迟时间(应用backoff)
current_delay *= backoff
return wrapper
return decorator
3.2 代码逐行解析与实战测试
让我们看看这个装饰器是怎么工作的,并立刻用它来模拟一个真实场景。
场景模拟:我们有一个函数 query_payment_status(payment_id),它去调用一个不太稳定的支付网关。单次调用可能因为网络问题 (ConnectionError) 或网关处理慢 (TimeoutError) 而失败。我们要求这个查询操作必须在 15秒 内拿到结果。
import random
import requests
# 使用我们自定义的超时重试装饰器
@retry_with_timeout(
exceptions=(requests.exceptions.ConnectionError, requests.exceptions.Timeout),
timeout=15.0, # 总超时15秒
delay=1, # 第一次重试等1秒
backoff=2, # 指数退避
max_delay=5, # 最多等5秒
jitter=(0, 0.5) # 加一点随机抖动
)
def query_payment_status(payment_id: str) -> dict:
"""模拟查询支付状态,有较高失败概率"""
# 模拟各种故障
rand_val = random.random()
if rand_val < 0.4:
# 40% 概率模拟连接错误
raise requests.exceptions.ConnectionError(f"模拟连接失败 for {payment_id}")
elif rand_val < 0.7:
# 30% 概率模拟超时
raise requests.exceptions.Timeout(f"模拟请求超时 for {payment_id}")
else:
# 30% 概率成功
return {"status": "SUCCESS", "payment_id": payment_id, "amount": 100}
# 测试
if __name__ == "__main__":
try:
result = query_payment_status("pay_123456")
print(f"查询成功!结果: {result}")
except Exception as e:
print(f"最终失败: {type(e).__name__}: {e}")
运行这段代码,你可能会看到类似如下的输出(因为引入了随机性):
[query_payment_status] 第 1 次尝试失败,错误: 模拟连接失败 for pay_123456。等待 1.23 秒后重试... (剩余超时: 13.77秒)
[query_payment_status] 第 2 次尝试失败,错误: 模拟请求超时 for pay_123456。等待 2.87 秒后重试... (剩余超时: 10.90秒)
[query_payment_status] 第 3 次尝试失败,错误: 模拟连接失败 for pay_123456。等待 5.00 秒后重试... (剩余超时: 5.90秒)
[query_payment_status] 第 4 次尝试失败,错误: 模拟请求超时 for pay_123456。等待 5.00 秒后重试... (剩余超时: 0.90秒)
[query_payment_status] 在 15.0 秒超时后仍失败,已尝试 5 次。最后错误: 模拟请求超时 for pay_123456
最终失败: Timeout: 模拟请求超时 for pay_123456
关键点解析:
- 时间感知:装饰器内部持续计算
remaining = end_time - now。第四次重试时,计算出的等待时间本来是current_delay * backoff = 5 * 2 = 10秒,但remaining只剩 0.9 秒,所以实际只等待了 0.9 秒。这是“总超时”控制的核心。 - 退避与上限:延迟从1秒开始,按
backoff=2增长:1秒,2秒,4秒,8秒… 但被max_delay=5限制,所以第三次及以后的等待上限都是5秒。 - 随机抖动:
jitter=(0, 0.5)使得每次等待时间在[计算值, 计算值+0.5]秒之间随机,避免了重试节奏同步。 - 清晰的日志:日志记录了尝试次数、错误原因、等待时间和剩余超时时间,这对于调试和监控至关重要。
4. 进阶融合:将超时控制与原版retry结合
我们上面自己写的 retry_with_timeout 已经很好用了,但它和社区标准的 retry 库是两套东西。有时候,我们可能既想用 retry 库丰富的生态和参数(比如它处理 logger 的方式更成熟),又想加入超时控制。有没有办法“鱼与熊掌兼得”呢?
答案是肯定的。我们可以采用 “装饰器嵌套” 或者 “功能增强” 的思路。这里我展示一种更优雅的“增强”思路:继承或包装原版 retry 装饰器,在其重试判断逻辑中加入超时检查。
不过,由于 retry 库的内部逻辑并不直接暴露时间判断的接口,一个更直接且实用的方法是:将超时控制作为一个更外层的“总闸”,内部依然使用 @retry 进行基于次数的精细控制。这样,当总时间耗尽时,外层的“闸门”会强制终止整个重试过程。
下面是一个结合两者的示例,我称之为 retry_hybrid:
import time
from retry import retry as original_retry
from functools import wraps
def retry_hybrid(
timeout: float = 30.0,
retry_kwargs: dict = None,
):
"""
一个混合重试装饰器,结合了总超时控制和原版retry的丰富参数。
先应用原版retry进行次数/退避控制,再在外层加上总时间限制。
:param timeout: 总操作超时时间(秒)。
:param retry_kwargs: 传递给原版 `@retry` 装饰器的参数字典。
"""
if retry_kwargs is None:
retry_kwargs = {}
def outer_decorator(func):
# 首先,用原版retry装饰函数
inner_retried_func = original_retry(**retry_kwargs)(func)
@wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
end_time = start_time + timeout
# 定义一个会检查超时的异常处理器
def timeout_aware_call():
# 每次调用前检查是否超时
if time.time() >= end_time:
raise TimeoutError(f"操作总时长超过 {timeout} 秒,强制终止。")
return inner_retried_func(*args, **kwargs)
# 理论上,我们应该在这里循环调用 timeout_aware_call。
# 但原版retry内部已经包含了循环,我们无法直接插入每次循环前的检查。
# 因此,这个“混合”方案更适用于:将超时检查作为原版retry可能抛出的一种异常。
# 我们可以修改思路:让原版retry也捕获TimeoutError,并在超时时立即停止。
# 但原版retry的循环我们控制不了。所以,更彻底的方案是修改我们自己的 `retry_with_timeout`,使其参数和原版retry兼容。
# 这里提供一个简化思路:如果原版retry最终失败(次数用尽),会抛出异常。
# 我们在这个外层捕获它,如果是因为超时,则转换异常类型。
# 但这不是真正的“在重试过程中检查超时”。
# 因此,对于严格的超时控制,建议直接使用我们自制的 `retry_with_timeout`。
# 以下代码演示一个“软”结合:
try:
return inner_retried_func(*args, **kwargs)
except Exception as e:
if time.time() >= end_time:
# 如果异常发生时已经超时,则抛出一个超时异常
raise TimeoutError(f"操作在重试过程中超时(总时长>{timeout}秒)。最后错误: {e}") from e
else:
# 如果没超时,那说明是原版retry次数用尽了,直接抛出原异常
raise
return wrapper
return outer_decorator
这个 retry_hybrid 的局限性在于,它无法在 original_retry 的每一次重试间隔前精确检查总超时。它更像是一个“事后检查”。要实现真正的、在每次重试前都检查总超时的融合,更好的办法是 以我们自制的 retry_with_timeout 为基础,吸收原版 retry 的所有参数和逻辑。这需要更深入地重写重试循环,但好处是能获得完全的控制权。
考虑到文章的实用性,我建议在需要严格超时控制的场景下,直接使用我们自制的 retry_with_timeout,并参照原版 retry 的参数风格进行扩展。这样代码更清晰,行为也更可预测。retry_hybrid 的思路可以作为一种启发,用于其他需要组合多个装饰器或库功能的场景。
5. 生产环境实战:网络请求与自动化测试案例
理论讲完了,我们来点实在的。看看在真实的开发场景中,如何应用这些重试策略。
5.1 案例一:带超时和退避的HTTP API客户端
假设你要调用一个第三方天气API。这个API偶尔会返回5xx错误(服务器内部错误)或者连接超时。对于5xx错误,通常重试是有效的。我们设计一个策略:总超时60秒,单次请求超时10秒,遇到5xx错误或连接问题时重试,采用指数退避。
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
import logging
# 方案A:使用requests库内置的Retry机制(基于urllib3)
# 这个机制是作用于HTTP适配器层的,更底层,但配置相对固定。
def create_robust_session():
session = requests.Session()
retry_strategy = Retry(
total=5, # 最大重试次数(包含初始请求)
backoff_factor=1, # 退避因子:{backoff factor} * (2 ** ({retry number} - 1))
status_forcelist=[500, 502, 503, 504], # 遇到这些状态码会重试
allowed_methods=["GET", "POST"] # 只对这些HTTP方法重试
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("http://", adapter)
session.mount("https://", adapter)
return session
# 使用内置Retry
session = create_robust_session()
try:
# 这里设置的是单次请求的超时
response = session.get("https://api.weather.com/v1/forecast", timeout=10)
response.raise_for_status()
except requests.exceptions.RequestException as e:
print(f"请求最终失败: {e}")
# 注意:urllib3的Retry是基于次数的,不直接支持总超时控制。
# 方案B:使用我们自制的超时重试装饰器(更灵活,支持总超时)
@retry_with_timeout(
exceptions=(requests.exceptions.HTTPError, requests.exceptions.ConnectionError, requests.exceptions.Timeout),
timeout=60.0,
delay=2,
backoff=2,
max_delay=30,
jitter=1,
logger=logging.getLogger(__name__)
)
def get_weather_with_retry(city: str):
"""获取天气信息,带有总超时控制的健壮重试"""
# 注意:这里我们为每次尝试单独设置超时
response = requests.get(f"https://api.weather.com/v1/forecast?city={city}", timeout=10)
# raise_for_status() 会在状态码不是200时抛出HTTPError,被我们的装饰器捕获
response.raise_for_status()
return response.json()
# 使用自定义装饰器
try:
weather_data = get_weather_with_retry("Beijing")
print(f"成功获取天气数据: {weather_data}")
except Exception as e:
print(f"在60秒内未能成功获取天气数据: {e}")
两种方案对比:
urllib3.Retry:集成在requests底层,对HTTP状态码的重试支持好,但配置灵活性较低,难以实现“总操作时间超时”这种业务逻辑。- 自定义
@retry_with_timeout:位于业务逻辑层,可以封装任意可能失败的操作(不限于HTTP),能精确控制总时间,策略定制更自由。在生产中,我通常推荐使用自定义装饰器,因为它能更好地表达业务意图。
5.2 案例二:Selenium自动化测试中的元素等待与重试
UI自动化测试是重试机制的另一大用武之地。页面元素加载受网络、前端渲染影响,极不稳定。WebDriverWait 提供了“显式等待”,但有时我们需要更复杂的重试逻辑,比如在等待元素出现的同时,执行一些清理操作或检查其他条件。
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import NoSuchElementException, StaleElementReferenceException, TimeoutException
# 假设我们有一个需要重试的测试步骤:点击一个动态加载的按钮,并验证结果。
driver = webdriver.Chrome()
@retry_with_timeout(
exceptions=(NoSuchElementException, StaleElementReferenceException, AssertionError),
timeout=20,
delay=1,
backoff=1.5,
max_delay=5,
jitter=0.5
)
def retry_click_and_verify(button_id: str, expected_text: str):
"""重试点击按钮并验证页面文本,总超时20秒"""
# 1. 查找并点击按钮
button = driver.find_element(By.ID, button_id)
button.click()
# 2. 等待结果出现并验证(这里可以集成WebDriverWait)
# 我们使用显式等待,但将其包裹在重试装饰器中,以处理更复杂的失败场景
wait = WebDriverWait(driver, 5) # 单次等待最多5秒
result_element = wait.until(
EC.presence_of_element_located((By.ID, "result-message"))
)
actual_text = result_element.text
# 如果验证失败,抛出AssertionError,触发重试
assert actual_text == expected_text, f"文本不匹配。期望: '{expected_text}', 实际: '{actual_text}'"
return actual_text
# 使用示例
driver.get("https://your-test-page.com")
try:
final_text = retry_click_and_verify("submit-btn", "操作成功!")
print(f"验证通过,最终文本: {final_text}")
except TimeoutException:
print("在20秒内未能成功点击按钮或验证结果。")
except Exception as e:
print(f"其他错误: {e}")
finally:
driver.quit()
在这个例子中,装饰器不仅重试元素查找失败 (NoSuchElementException),还重试元素状态过期 (StaleElementReferenceException),甚至重试我们自定义的断言失败 (AssertionError)。这比单纯的 WebDriverWait 更强大,因为它将“操作-验证”这个完整流程作为一个可重试的单元,并且有总时间限制。
6. 避坑指南与最佳实践
重试用得好是利器,用不好就是灾难。下面是我总结的几个关键原则和常见陷阱。
1. 只重试“幂等”操作
这是铁律。幂等意味着多次执行相同的操作,产生的副作用是一样的。比如查询(GET)、根据ID更新(PUT)通常是幂等的。而创建订单(POST)、付款(POST)通常不是幂等的。对非幂等操作重试,可能导致重复创建订单、重复扣款等严重问题。在装饰器里,务必通过 exceptions 参数严格限定只对网络波动、临时性错误进行重试,对于业务逻辑错误(如余额不足)不应重试。
2. 设置合理的上限:次数与时间
无限重试 (tries=-1 或 timeout=None) 非常危险,可能使线程/进程永远挂起。一定要设置一个绝对上限,无论是次数(如10次)还是时间(如30秒)。并且,最终失败时,一定要抛出清晰的异常,让上游调用者知道是“重试后仍失败”,而不是第一次就失败了。
3. 退避(Backoff)与抖动(Jitter)是必须的 立即、固定间隔的重试,很容易在服务短暂故障恢复时,被瞬间涌来的重试请求再次打垮。指数退避给了服务恢复的时间。随机抖动避免了所有客户端同时重试,将流量从“脉冲式”变为“平滑式”。这两个策略能极大提升整个系统的稳定性。
4. 区分错误类型:什么该重试,什么不该
- 应该重试:连接超时 (
TimeoutError)、连接被拒绝 (ConnectionRefusedError)、HTTP 5xx 错误(服务器内部错误)、短暂的死锁或资源竞争异常。 - 不应该重试:HTTP 4xx 错误(客户端错误,如404 Not Found, 400 Bad Request)。客户端错误重试再多次也没用。业务逻辑错误(如验证失败、权限不足)。程序Bug(如
TypeError,ValueError)。
5. 记录每一次重试 日志是排查问题的生命线。确保你的重试装饰器记录了每一次重试尝试,包括时间、错误信息、等待时长、剩余重试次数或时间。这能帮你区分是偶发性问题还是持续性故障,并评估重试策略的有效性。
6. 考虑“断路器”模式
对于频繁失败的服务,持续重试可能浪费资源。更高级的模式是“断路器”(Circuit Breaker):当失败次数超过阈值,断路器“跳闸”,短时间内直接拒绝所有请求(快速失败),给下游服务喘息之机。一段时间后,再进入“半开”状态试探性放行少量请求,如果成功则闭合断路器恢复调用。这可以与重试模式结合使用,形成更强大的弹性架构。虽然 retry 库本身不提供断路器,但你可以结合 pybreaker 这类库来实现。
最后,记住一点:重试是提高系统容错性的手段,而不是掩盖问题的遮羞布。如果某个操作频繁失败到需要不断重试,那更应该去排查根本原因,比如服务是否健康、资源是否充足、代码是否有Bug。重试机制是你的安全网,但你不能总指望它来接住你。

1882

被折叠的 条评论
为什么被折叠?



