请求重试封装题
自测题
完成以下 3 道题目,检验你的学习成果
问题 1
哪些错误应该被归类为可重试错误?
解析:可重试错误包括网络超时、连接异常、5xx 服务端错误。4xx 客户端错误、业务异常(如余额不足)、认证失败通常不可重试。
问题 2
对支付、下单等写操作重试时需要注意什么?
解析:写操作重试前必须确认幂等性,否则第一次请求可能已成功,重试导致重复扣款或重复下单。方案:使用幂等键、服务端去重、或改用异步补偿机制。
问题 3
为什么重试需要使用退避策略?
解析:使用指数退避策略(如 100ms → 200ms → 400ms),加上随机抖动避免惊群效应。服务可能正在恢复中,立即重试会加剧压力,大量请求同时重试可能造成雪崩。
测验结果
题目背景
理解面试官出这道题的意图
这道题主要考察三个能力:系统稳定性意识、工程化思维和边界处理能力。系统稳定性意识体现在是否理解分布式系统的不确定性——网络抖动、服务限流、依赖超时都是常态,重试是提升可用性的基础手段。工程化思维体现在能否将重试抽象为可配置、可观测、可控制的模块,而不是写死在业务代码里。边界处理能力体现在是否考虑了幂等性、超时控制、熔断机制、日志追踪等生产环境必备的保障措施。题目类型属于设计类+实现类:先要讲清楚设计思路(什么情况下重试、怎么重试、重试几次),再给出核心实现逻辑。面试官通常不会要求完整代码,更看重你能否讲清楚关键决策点。
解题思路
错误分类→策略选择→退避设计→边界处理
- 第一步:错误分类——把错误分为可重试和不可重试两类。
网络超时、连接异常、5xx 服务端错误通常可重试。4xx 客户端错误、业务异常(如余额不足)通常不可重试。
- 第二步:策略选择——根据业务场景选择重试策略。
读操作可以放心重试,写操作需要考虑幂等性。同步重试适合快速恢复,异步补偿适合最终一致性场景。
- 第三步:退避设计——设计退避间隔避免重试风暴。固定间隔简单但可能造成拥塞,指数退避更合理,可加随机抖动避免惊群效应。
- 第四步:边界处理——设置最大重试次数、总体超时时间、熔断阈值。记录每次重试的上下文信息,方便排查问题。
- 设计权衡:重试次数多→恢复概率大但延迟增加。退避间隔短→响应快但可能加剧拥塞。同步重试→逻辑简单但阻塞线程。异步补偿→解耦但复杂度高。
示例代码:重试机制封装
# utils/retry.py - 重试机制封装import timeimport randomimport loggingfrom typing import Callable, Any, Typefrom functools import wraps
logger = logging.getLogger(__name__)
# 可重试的异常类型RETRYABLE_EXCEPTIONS = (ConnectionError, TimeoutError, OSError)
def retry( max_attempts: int = 3, base_delay: float = 1.0, max_delay: float = 30.0, retryable_exceptions: tuple[Type[Exception], ...] = RETRYABLE_EXCEPTIONS,): """重试装饰器 - 支持指数退避和随机抖动""" def decorator(func: Callable) -> Callable: @wraps(func) def wrapper(*args, **kwargs) -> Any: last_exception = None for attempt in range(1, max_attempts + 1): try: return func(*args, **kwargs) except retryable_exceptions as e: last_exception = e if attempt == max_attempts: logger.error( f"{func.__name__} 重试 {max_attempts} 次后失败: {e}" ) raise
# 指数退避 + 随机抖动 delay = min(base_delay * (2 ** (attempt - 1)), max_delay) jitter = random.uniform(0, delay * 0.1) sleep_time = delay + jitter
logger.warning( f"{func.__name__} 第 {attempt} 次失败: {e}, " f"{sleep_time:.2f}s 后重试..." ) time.sleep(sleep_time)
raise last_exception # type: ignore return wrapper return decorator
# 使用示例@retry(max_attempts=3, base_delay=1.0)def fetch_data(url: str) -> dict: """获取数据,自动重试""" import requests resp = requests.get(url, timeout=10) resp.raise_for_status() return resp.json()# 使用 tenacity 库(生产推荐)from tenacity import ( retry, stop_after_attempt, wait_exponential, retry_if_exception_type, before_log, after_log,)import logging
logger = logging.getLogger(__name__)
@retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=10), retry=retry_if_exception_type((ConnectionError, TimeoutError)), before=before_log(logger, logging.WARNING), after=after_log(logger, logging.INFO),)def call_api(url: str) -> dict: """使用 tenacity 的重试装饰器""" import requests return requests.get(url, timeout=10).json()代码逻辑
核心流程描述,不展示完整代码
【整体流程】入口函数接收请求参数 → 调用执行器执行请求 → 捕获异常 → 判断是否可重试 → 可重试则计算退避时间并等待 → 再次执行 → 达到上限或成功则返回 → 记录完整日志。【核心步骤详解】1. 可重试判断器:接收异常对象,根据异常类型和状态码返回是否可重试。\n\n例如:连接超时返回 true,认证失败返回 false。2. 退避策略器:接收当前重试次数,返回等待时间。指数退避公式:base * 2^attempt,加上随机抖动避免同步重试。3. 重试执行器:循环执行请求,每次失败后调用判断器和策略器,成功或达到上限则退出。总体超时控制在外层包装。4. 日志记录器:每次重试记录时间戳、重试次数、异常类型、退避时间、最终结果。输出结构化日志便于检索分析。【关键接口定义】RetryConfig:包含最大次数、初始间隔、最大间隔、可重试异常列表。RetryContext:包含当前次数、总耗时、上次异常、请求参数。RetryResult:包含最终状态、总次数、总耗时、失败原因链。
常见失分点
面试中最容易丢分的 5 个问题
失分点 1:不分错误类型,全部重试
错误做法:捕获所有异常后直接重试,不区分错误原因。
为什么不好:业务错误(如余额不足)重试会重复扣款。认证错误重试毫无意义且可能触发风控。
如何改进:建立错误分类器,根据异常类型和状态码判断是否可重试。
常见规则:网络层异常可重试,业务层异常不重试。
失分点 2:没有退避策略,立即重试
错误做法:失败后立即重试,循环执行。
为什么不好:服务可能正在恢复中,立即重试会加剧压力。大量请求同时重试可能造成雪崩。
如何改进:使用指数退避策略,每次重试间隔翻倍。
例如:100ms → 200ms → 400ms → 800ms,加上随机抖动。
失分点 3:忽略幂等性风险
错误做法:对支付、下单等写操作直接重试,不考虑副作用。
为什么不好:第一次请求可能已成功,重试导致重复扣款或重复下单。
如何改进:写操作重试前必须确认幂等性。
方案:使用幂等键(idempotency key)、服务端去重、或改用异步补偿机制。
失分点 4:没有超时控制
错误做法:只设置重试次数,不控制总体超时时间。
为什么不好:极端情况下重试累积时间可能超过业务可接受范围,用户等待超时。
如何改进:设置总体超时时间,超时后终止重试并返回错误。
总体超时 = 单次超时 + 重试次数 × 最大退避时间。
失分点 5:没有日志和可观测性
错误做法:重试过程无日志,只在最后返回成功或失败。
为什么不好:生产问题排查时无法知道重试了几次、每次是什么错误、退避了多久。
如何改进:每次重试记录结构化日志,包含:时间戳、重试次数、异常类型、退避时间、请求标识。
可接入监控系统统计重试率和成功率。
进阶讨论
展示技术深度和系统思维
【性能考虑】重试对系统的影响是双面的。正向看,重试提高了请求成功率,减少了因瞬时故障导致的业务失败。负向看,重试增加了后端压力,特别是故障恢复期可能形成重试风暴。
生产环境建议:限制单机并发重试数、配合熔断器快速失败、重要接口降级到异步队列。【扩展性设计】可配置的重试策略让系统更灵活。核心配置项:最大重试次数、退避策略类型(固定/线性/指数)、初始间隔、最大间隔、可重试异常列表、总体超时时间。
配置方式:代码默认值 + 配置文件覆盖 + 动态调整(结合配置中心)。
不同接口可有不同策略,例如支付接口重试次数少但间隔短,数据同步接口重试次数多但间隔长。【方案对比】同步重试 vs 异步补偿:同步重试适用于快速恢复场景,用户等待但逻辑简单。异步补偿适用于最终一致性场景,用户无感知但需要消息队列和状态机。内置重试 vs 框架重试:内置重试可控性强但需要自己实现。框架重试(如 tenacity、resilience4j)功能完善但需要理解框架机制。单机重试 vs 分布式重试:单机重试简单但重启后丢失。分布式重试(消息队列)可靠但复杂度高。
自测题
完成以下 3 道题目,检验你的学习成果
问题 1
哪些错误应该被归类为可重试错误?
解析:可重试错误包括网络超时、连接异常、5xx 服务端错误。4xx 客户端错误、业务异常(如余额不足)、认证失败通常不可重试。
问题 2
对支付、下单等写操作重试时需要注意什么?
解析:写操作重试前必须确认幂等性,否则第一次请求可能已成功,重试导致重复扣款或重复下单。方案:使用幂等键、服务端去重、或改用异步补偿机制。
问题 3
为什么重试需要使用退避策略?
解析:使用指数退避策略(如 100ms → 200ms → 400ms),加上随机抖动避免惊群效应。服务可能正在恢复中,立即重试会加剧压力,大量请求同时重试可能造成雪崩。