4、重试策略基础:固定间隔重试策略的原理与实现
说到重试策略,最简单也最直观的,就是固定间隔重试。说白了,就是每次失败后,等一个固定的时间,然后再试一次。我刚开始做分布式系统的时候,第一个重试方案用的就是这个。
你可能会想,这也太简单了吧?嗯,确实简单。但简单不代表没用。很多场景下,固定间隔反而是最稳的选择。比如下游服务正在重启,或者网络抖动,你等个几秒再试,往往就通了。
4.1 固定间隔的核心逻辑
固定间隔重试的流程,其实就三个步骤:
- 执行任务 — 发起一次调用
- 判断结果 — 如果成功,结束;如果失败,进入下一步
- 等待固定时长 — 睡一会儿,然后回到第1步
直到达到最大重试次数,或者任务成功为止。
核心参数就两个:
- 重试间隔(retryInterval) — 每次等待的固定时长,单位通常是毫秒
- 最大重试次数(maxRetries) — 最多重试多少次,防止无限循环
我见过不少新手把最大重试次数设成 Integer.MAX_VALUE,结果线上出了个 bug,日志里全是重试记录,磁盘直接打满。嗯,这个坑我踩过。
4.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 固定间隔的优缺点分析
任何策略都有适用场景。固定间隔重试也不例外。我整理了一张表,方便你对比。
| 维度 | 说明 |
|---|---|
| 优点 |
|
| 缺点 |
|
避坑指南:我曾经在一个高并发项目里用了固定间隔重试,间隔设了 1 秒。结果下游服务短暂抖动时,上千个请求同时进入等待,1 秒后同时发起重试,直接把下游打挂了。后来我加了随机抖动(jitter),才解决这个问题。后面章节会详细讲抖动,这里先记住:固定间隔在高并发场景下要谨慎使用。
4.5 什么时候该用固定间隔?
根据我的经验,以下场景用固定间隔比较合适:
- 非关键路径的异步任务 — 比如发送通知邮件,失败了等 10 秒再试,不影响主流程
- 已知故障恢复时间 — 比如你知道下游服务重启需要 5 秒,那就设 5 秒间隔
- 并发量低的系统 — 每秒几十个请求,固定间隔完全够用
- 快速验证场景 — 开发环境调试时,固定间隔最直观
反过来,如果你的系统并发高、下游不稳定、或者故障恢复时间不确定,那固定间隔就不太够用了。这时候可以考虑指数退避或者带抖动的策略。
4.6 实现时的几个细节
写固定间隔重试时,有几个小细节容易被忽略:
- 中断处理 — Thread.sleep() 会响应中断,记得恢复中断状态。不然线程池关闭时可能卡住
- 异常传递 — 最后一次异常一定要带上,方便排查问题
- 重试次数计数 — 注意是「重试次数」还是「总尝试次数」。我习惯把第一次调用也算在尝试次数里,这样逻辑更清晰
- 日志输出 — 每次重试失败都打一条日志,但别打堆栈,打关键信息就行。不然日志量会很大
一个常见的错误写法:
// 错误:没有判断重试次数,会无限重试
while (true) {
try {
return task.call();
} catch (Exception e) {
Thread.sleep(1000);
}
}
这种代码一旦遇到持续故障,线程就永远卡在里面了。一定要设最大重试次数。
4.7 小结
固定间隔重试,是所有重试策略里最基础的一种。它简单、直观、容易实现。但正因为简单,它的局限性也很明显。你想想看,如果所有场景都用固定间隔,那系统稳定性反而可能出问题。
我个人建议:先掌握固定间隔,把它作为重试策略的入门。然后根据实际场景,逐步引入更复杂的策略。下一章我们会讲指数退避,那才是应对长时间故障的利器。
嗯,今天就到这里。代码你可以直接拿去用,但记得根据你的业务场景调整参数。别把重试间隔设得太短,也别把重试次数设得太多。适可而止,才是工程智慧。