本文目录导读:
网络编程中处理重试是一个常见的挑战,因为网络环境不可靠(如超时、丢包、服务暂时不可用),设计良好的重试机制可以显著提高系统的健壮性,但设计不当(如无限制重试)可能导致雪崩效应或资源浪费。
以下是处理网络重试的核心原则、常见策略及代码示例:
核心原则
- 幂等性:这是重试的前提条件,如果操作本身不是幂等的(创建订单、扣款),重试可能导致业务数据错误,对于非幂等操作,需要在上层设计去重机制(如请求ID)。
- 退避策略:不要立即重试,要有一定的等待时间,给服务恢复的时间。
- 有限次重试:设定最大重试次数,避免无限循环。
- 抖动:在退避时间上增加随机性,防止所有客户端在同一时间重试,导致服务器再次崩溃(惊群效应)。
- 超时管理:每一次重试都应该有独立的超时时间,且总耗时不能过长。
常见重试策略
固定间隔重试
最简单,但效果较差。
- 逻辑:每次重试等待相同的时间(如 1秒)。
- 缺点:如果服务需要较长时间恢复,固定间隔会浪费客户端和服务器资源。
指数退避
最常用且经典。
-
逻辑:等待时间随重试次数指数增长,例如第1次等 100ms,第2次等 200ms,第3次等 400ms... (2^n * baseInterval)。
-
代码示例 (Go 语言):
import ( "fmt" "time" "net/http" ) func doRequestWithRetry(url string, maxRetries int) (*http.Response, error) { var resp *http.Response var err error for i := 0; i < maxRetries; i++ { resp, err = http.Get(url) // 实际中建议复用Client if err == nil { return resp, nil } // 指数退避并添加抖动 sleepDuration := time.Duration(1 << uint(i)) * time.Second // 2^i 秒 jitter := time.Duration(rand.Intn(100)) * time.Millisecond fmt.Printf("Attempt %d failed: %v. Retrying in %v...\n", i+1, err, sleepDuration+jitter) time.Sleep(sleepDuration + jitter) } return nil, fmt.Errorf("all %d retries failed: %w", maxRetries, err) }
指数退避 + 随机抖动 (Jitter)
在指数退避基础上增加了随机延迟,避免惊群。
-
逻辑:
sleep = min(cap, base * 2^attempt) + random(0, jitter_base) -
示例 (Python with
tenacity库):import tenacity import requests import random @tenacity.retry( stop=tenacity.stop_after_attempt(3), # 最多重试3次 wait=tenacity.wait_exponential(multiplier=1, min=1, max=10) + tenacity.wait_random(min=0, max=1), retry=tenacity.retry_if_exception_type(requests.exceptions.ConnectionError) ) def fetch_data(url): response = requests.get(url, timeout=5) response.raise_for_status() return response.json()
增量退避
- 逻辑:每次增加一个固定步长(如 100ms, 200ms, 300ms...),较少使用,适用于网络抖动但服务器能快速恢复的场景。
重试边界条件:哪些错误值得重试?
不是所有错误都适合重试,需要区分错误类型:
✅ 应该重试的错误(临时性、可恢复):
- 网络超时 (
Timeout) - 连接被拒绝 (
Connection refused,通常是服务刚重启或重启中) - DNS 解析失败 (
DNS lookup failed) - HTTP 5xx 状态码 (服务器错误,如 503 Service Unavailable, 502 Bad Gateway, 500 Internal Server Error)
❌ 不应该重试的错误(需要人工介入或逻辑错误):
- HTTP 4xx 状态码 (客户端错误):4xx 意味着请求本身有问题(如 401 未授权, 403 禁止访问, 404 不存在, 422 参数校验失败),重试 100 次还是 404,不会改变结果。
- 连接已断开 (
broken pipe或connection reset在特定业务下可能重试,但需谨慎)。
高级重试模式
断路器模式 (Circuit Breaker)
防止在服务明显不可用时,客户端仍不断重试导致雪崩(重试雪崩)。
- 状态:
Closed(正常) ->Open(故障率高,立即拒绝请求) ->Half-Open(尝试放行一个请求看是否恢复)。 - 实现:在累加连续失败次数超过阈值后,直接失败,不再发起网络请求。
基于预算的重试 (Retry Budget)
限制在一个时间窗口内(1 分钟)允许的总重试次数(如最多重试总请求数的 10%),避免重试流量淹没系统。
异步重试
- 同步重试:调用方等待重试结果(适合短时故障)。
- 异步重试(如消息队列):将请求放入队列,由后台 Worker 负责重试,业务方无需阻塞等待,适合长时间跨度的重试(如支付宝支付失败后,系统会在 30 分钟内自动重试若干次)。
工程实现示例(Python)
使用 tenacity 库实现一个健壮的重试辅助函数:
import requests
import tenacity
from tenacity import stop_after_attempt, wait_exponential, retry_if_exception, before_sleep_log
import logging
logging.basicConfig(level=logging.INFO)
# 定义哪些异常需要重试
def is_retryable_exception(exception):
# 只重试连接错误和超时,不重试4xx
if isinstance(exception, requests.exceptions.ConnectionError):
return True
if isinstance(exception, requests.exceptions.Timeout):
return True
if isinstance(exception, requests.exceptions.HTTPError):
if exception.response.status_code in (502, 503, 504):
return True
return False
@tenacity.retry(
stop=stop_after_attempt(3), # 重试3次
wait=wait_exponential(multiplier=1, min=1, max=10), # 1, 2, 4秒后重试
retry=retry_if_exception(is_retryable_exception), # 只有可重试异常才重试
before_sleep=before_sleep_log(logging.getLogger("retry"), logging.WARNING),
reraise=True # 重试耗尽后抛出原始异常
)
def fetch_critical_data(url, headers):
response = requests.get(url, headers=headers, timeout=(3, 10)) # connect_timeout=3s, read_timeout=10s
response.raise_for_status()
return response.json()
| 维度 | 推荐做法 |
|---|---|
| 重试前提 | 必须幂等或设计去重机制(请求ID) |
| 错误判断 | 只重试网络超时、连接拒绝、5xx;不重试4xx |
| 等待策略 | 指数退避 + 随机抖动 (最通用) |
| 最大次数 | 2-5 次(取决于业务容忍度),通常不超过 3 次 |
| 并发保护 | 结合断路器 或 重试预算,避免雪崩 |
| 幂等实现 | 请求体中包含 idempotency_key(幂等键),服务端校验去重 |
最后一点建议:在设计重试时,请始终反问自己:“如果这次重试触发了服务器故障恢复的高负载峰值,系统能否承受?” 如果能加上熔断和限流,重试机制会更安全。
标签: 超时重试