4、重试策略基础:固定间隔重试策略的原理与实现

说到重试策略,最简单也最直观的,就是固定间隔重试。说白了,就是每次失败后,等一个固定的时间,然后再试一次。我刚开始做分布式系统的时候,第一个重试方案用的就是这个。

你可能会想,这也太简单了吧?嗯,确实简单。但简单不代表没用。很多场景下,固定间隔反而是最稳的选择。比如下游服务正在重启,或者网络抖动,你等个几秒再试,往往就通了。

4.1 固定间隔的核心逻辑

固定间隔重试的流程,其实就三个步骤:

  1. 执行任务 — 发起一次调用
  2. 判断结果 — 如果成功,结束;如果失败,进入下一步
  3. 等待固定时长 — 睡一会儿,然后回到第1步

直到达到最大重试次数,或者任务成功为止。

核心参数就两个:

  • 重试间隔(retryInterval) — 每次等待的固定时长,单位通常是毫秒
  • 最大重试次数(maxRetries) — 最多重试多少次,防止无限循环

我见过不少新手把最大重试次数设成 Integer.MAX_VALUE,结果线上出了个 bug,日志里全是重试记录,磁盘直接打满。嗯,这个坑我踩过。

4.2 流程图:固定间隔重试的完整生命周期

下面这张图,把整个流程画清楚了。你可以对照着看,每一步都对应一个节点。

固定间隔重试策略流程图 固定间隔重试策略流程图 开始执行任务 执行任务调用 成功? 判断结果 是 返回成功 否 已达最大 重试次数? 是 失败 否 等待固定间隔 (如 2 秒) 重试循环 关键点 • 每次失败后等待时间相同 • 达到最大次数后不再重试

4.3 代码实现:一个简洁的固定间隔重试器

理论说完了,咱们直接看代码。我个人习惯把重试逻辑封装成一个工具类,这样业务代码里只需要一行调用。

public class FixedIntervalRetry {

    private final long retryInterval;  // 重试间隔,单位毫秒
    private final int maxRetries;      // 最大重试次数

    public FixedIntervalRetry(long retryInterval, int maxRetries) {
        if (retryInterval <= 0) {
            throw new IllegalArgumentException("retryInterval 必须大于 0");
        }
        if (maxRetries <= 0) {
            throw new IllegalArgumentException("maxRetries 必须大于 0");
        }
        this.retryInterval = retryInterval;
        this.maxRetries = maxRetries;
    }

    /**
     * 执行带重试的任务
     * @param task 需要重试的任务
     * @param <T>  返回值类型
     * @return 任务执行结果
     * @throws Exception 所有重试都失败后抛出最后一次异常
     */
    public <T> T execute(Callable<T> task) throws Exception {
        int attempt = 0;
        Exception lastException = null;

        while (attempt <= maxRetries) {
            try {
                return task.call();
            } catch (Exception e) {
                attempt++;
                lastException = e;
                System.err.println("第 " + attempt + " 次尝试失败: " + e.getMessage());

                if (attempt >= maxRetries) {
                    break;
                }

                // 等待固定间隔
                try {
                    Thread.sleep(retryInterval);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    throw new RuntimeException("重试被中断", ie);
                }
            }
        }

        throw new RuntimeException("重试 " + maxRetries + " 次后仍然失败", lastException);
    }
}

使用示例:

FixedIntervalRetry retryer = new FixedIntervalRetry(2000, 3);
String result = retryer.execute(() -> {
    return httpClient.get("https://api.example.com/data");
});

这段代码的意思是:每次失败等 2 秒,最多重试 3 次。如果 3 次都失败,抛出异常。

4.4 固定间隔的优缺点分析

任何策略都有适用场景。固定间隔重试也不例外。我整理了一张表,方便你对比。

维度 说明
优点
  • 实现简单 — 几行代码就能搞定,不容易出错
  • 行为可预测 — 每次重试的间隔固定,便于排查问题
  • 适合瞬时故障 — 比如网络闪断、服务刚重启完
缺点
  • 可能造成雪崩 — 如果大量请求同时重试,下游瞬间被打爆
  • 不够灵活 — 故障持续时间长时,固定间隔可能白白浪费资源
  • 不适合长故障 — 比如数据库挂了 10 分钟,你每秒重试一次,除了刷日志没任何意义

避坑指南:我曾经在一个高并发项目里用了固定间隔重试,间隔设了 1 秒。结果下游服务短暂抖动时,上千个请求同时进入等待,1 秒后同时发起重试,直接把下游打挂了。后来我加了随机抖动(jitter),才解决这个问题。后面章节会详细讲抖动,这里先记住:固定间隔在高并发场景下要谨慎使用。

4.5 什么时候该用固定间隔?

根据我的经验,以下场景用固定间隔比较合适:

  • 非关键路径的异步任务 — 比如发送通知邮件,失败了等 10 秒再试,不影响主流程
  • 已知故障恢复时间 — 比如你知道下游服务重启需要 5 秒,那就设 5 秒间隔
  • 并发量低的系统 — 每秒几十个请求,固定间隔完全够用
  • 快速验证场景 — 开发环境调试时,固定间隔最直观

反过来,如果你的系统并发高、下游不稳定、或者故障恢复时间不确定,那固定间隔就不太够用了。这时候可以考虑指数退避或者带抖动的策略。

4.6 实现时的几个细节

写固定间隔重试时,有几个小细节容易被忽略:

  1. 中断处理 — Thread.sleep() 会响应中断,记得恢复中断状态。不然线程池关闭时可能卡住
  2. 异常传递 — 最后一次异常一定要带上,方便排查问题
  3. 重试次数计数 — 注意是「重试次数」还是「总尝试次数」。我习惯把第一次调用也算在尝试次数里,这样逻辑更清晰
  4. 日志输出 — 每次重试失败都打一条日志,但别打堆栈,打关键信息就行。不然日志量会很大

一个常见的错误写法:

// 错误:没有判断重试次数,会无限重试
while (true) {
    try {
        return task.call();
    } catch (Exception e) {
        Thread.sleep(1000);
    }
}

这种代码一旦遇到持续故障,线程就永远卡在里面了。一定要设最大重试次数。

4.7 小结

固定间隔重试,是所有重试策略里最基础的一种。它简单、直观、容易实现。但正因为简单,它的局限性也很明显。你想想看,如果所有场景都用固定间隔,那系统稳定性反而可能出问题。

我个人建议:先掌握固定间隔,把它作为重试策略的入门。然后根据实际场景,逐步引入更复杂的策略。下一章我们会讲指数退避,那才是应对长时间故障的利器。

嗯,今天就到这里。代码你可以直接拿去用,但记得根据你的业务场景调整参数。别把重试间隔设得太短,也别把重试次数设得太多。适可而止,才是工程智慧。