常见重试模式:从线性到渐进,你该选哪个?

重试策略听起来简单——失败了就再来一次呗。但实际落地时,你会发现「怎么重试」比「要不要重试」复杂得多。我见过太多团队直接写个 for i in range(3): try...except 就上线了,结果线上把数据库打崩了。

这一章,我们来拆解五种最常见的重试模式。每种模式都有它的适用场景,也有它的坑。我会结合自己的踩坑经历,帮你理清楚什么时候该用哪种。

核心观点:没有最好的重试模式,只有最合适的。选错了,比不重试更危险。

五种常见重试模式对比 固定间隔重试 间隔恒定 适合稳定负载 线性重试 间隔线性增长 可预测性高 指数退避 间隔指数增长 减轻下游压力 抖动指数退避 加入随机抖动 避免惊群效应 渐进式重试 阶梯式增长 灵活可控 选择原则:根据失败原因、系统负载、业务容忍度综合判断 没有银弹,只有权衡

固定间隔重试:简单但危险

固定间隔重试是最直观的模式。每次重试之间等待相同的时间,比如 1 秒、5 秒。实现起来几乎零成本:

import time

def retry_fixed_interval(func, max_retries=3, interval=2):
    for attempt in range(max_retries):
        try:
            return func()
        except Exception as e:
            if attempt == max_retries - 1:
                raise
            print(f"第 {attempt+1} 次失败,{interval}秒后重试...")
            time.sleep(interval)

这个模式最大的问题是什么?容易造成「踩踏效应」。我曾在一次线上事故中亲眼看到:100 个微服务实例同时检测到数据库连接失败,然后同时等待 5 秒,再同时重试——数据库直接被冲垮了。

避坑指南:固定间隔重试只适合以下场景:

  • 客户端数量极少(比如 1-3 个)
  • 失败原因是临时性的(如网络抖动)
  • 下游服务有足够的冗余容量

否则,请考虑其他模式。

线性重试:可预测的递增

线性重试让等待时间随着重试次数线性增长。比如第一次等 1 秒,第二次等 2 秒,第三次等 3 秒。这样做的好处是:给下游服务留出「喘息」的时间。

def retry_linear(func, max_retries=3, base_interval=1):
    for attempt in range(max_retries):
        try:
            return func()
        except Exception as e:
            if attempt == max_retries - 1:
                raise
            wait = base_interval * (attempt + 1)
            print(f"等待 {wait} 秒后重试...")
            time.sleep(wait)

我个人习惯在调用外部 API 时用线性重试。为什么?因为很多 API 的限流策略是「窗口式」的,线性增长能刚好避开限流窗口。不过要注意,如果重试次数太多,等待时间会变得很长——你想想看,第 10 次重试要等 10 秒,用户体验就崩了。

指数退避:业界标准方案

指数退避是分布式系统中最常用的重试模式。每次重试的等待时间按指数增长:1s、2s、4s、8s、16s……

def retry_exponential_backoff(func, max_retries=5, base=1, cap=60):
    for attempt in range(max_retries):
        try:
            return func()
        except Exception as e:
            if attempt == max_retries - 1:
                raise
            wait = min(base * (2 ** attempt), cap)
            print(f"等待 {wait} 秒后重试...")
            time.sleep(wait)

这里有个关键参数:cap(上限)。不加 cap 的话,第 10 次重试要等 512 秒,这显然不合理。我一般把 cap 设为 30 秒或 60 秒,具体看业务容忍度。

我的经验:指数退避最适合「下游服务过载」的场景。比如数据库连接池满了、消息队列堆积了。指数增长给了下游足够的时间来恢复。但要注意——如果所有客户端都用相同的指数退避策略,它们会在同一时间点重试,这就是「惊群效应」。

带抖动的指数退避:解决惊群效应

惊群效应的解法很简单:加随机抖动。在指数退避的基础上,给等待时间加上一个随机偏移量,让不同客户端的重试时间错开。

import random

def retry_with_jitter(func, max_retries=5, base=1, cap=60):
    for attempt in range(max_retries):
        try:
            return func()
        except Exception as e:
            if attempt == max_retries - 1:
                raise
            exponential = min(base * (2 ** attempt), cap)
            jitter = random.uniform(0, exponential * 0.5)
            wait = exponential + jitter
            print(f"等待 {wait:.2f} 秒后重试...")
            time.sleep(wait)

抖动有两种常见实现:

抖动类型 公式 特点
全抖动 random(0, exponential) 最大分散度,但等待时间不可预测
等比例抖动 exponential + random(0, exponential * 0.5) 保留指数增长趋势,同时错开时间

我推荐用等比例抖动。全抖动虽然分散效果好,但有时候等待时间会特别短,起不到「退避」的作用。等比例抖动在「退避」和「分散」之间取得了平衡。

核心建议:如果你的系统有超过 10 个客户端同时重试,请务必使用带抖动的指数退避。这是 AWS、Google Cloud 等云厂商的推荐做法。

渐进式重试:灵活的分段策略

渐进式重试不是简单的数学公式,而是分段定义的重试策略。你可以为不同的重试阶段设置不同的间隔和行为。

def retry_progressive(func, stages):
    """
    stages: [(interval, max_attempts), ...]
    示例: [(1, 2), (5, 3), (30, 5)]
    """
    total_attempts = 0
    for interval, max_attempts in stages:
        for _ in range(max_attempts):
            try:
                return func()
            except Exception as e:
                total_attempts += 1
                if total_attempts == sum(s[1] for s in stages):
                    raise
                print(f"等待 {interval} 秒后重试...")
                time.sleep(interval)

举个例子:

  • 第一阶段:每 1 秒重试,最多 2 次(应对瞬时故障)
  • 第二阶段:每 5 秒重试,最多 3 次(应对短暂恢复)
  • 第三阶段:每 30 秒重试,最多 5 次(应对慢恢复)

这种模式特别适合与外部第三方服务交互的场景。我记得有一次对接支付网关,对方文档里明确写了「前两次重试间隔 1 秒,之后间隔 10 秒」——这不就是渐进式重试嘛。

注意:渐进式重试的配置复杂度较高。你需要根据实际监控数据来调整每个阶段的参数。我建议先用指数退避兜底,等积累了足够的线上数据后,再优化为渐进式策略。

如何选择?一张表说清楚

模式 适用场景 风险 推荐指数
固定间隔 单客户端、低负载 踩踏效应 ★★☆☆☆
线性重试 API 限流场景 等待时间过长 ★★★☆☆
指数退避 通用场景、下游过载 惊群效应 ★★★★☆
抖动指数退避 多客户端、高并发 实现稍复杂 ★★★★★
渐进式重试 第三方服务、精细控制 配置复杂 ★★★★☆

我个人最常用的是带抖动的指数退避。它足够通用,实现简单,而且能应对大部分线上场景。只有在对接特定第三方服务时,我才会切换到渐进式重试。

嗯,这里要提醒一句:重试策略不是写出来就完事了。你需要监控重试的成功率、平均等待时间、对下游的影响。我曾经见过一个团队用了指数退避,但没设 cap,结果重试等待时间飙到 5 分钟——用户早跑了。