5、重试与退避模式:重试策略、风暴防范与幂等性设计
大家好,我是老张。今天我们来聊聊重试与退避模式。这个话题,说白了就是「失败了怎么办」。在分布式系统里,网络抖动、服务暂时不可用,这些都是家常便饭。你想想看,一个请求发出去,对方没响应,你是立刻再发一次?还是等一等?等多久?这些问题看似简单,但处理不好,系统分分钟雪崩。
我个人习惯把重试策略分成三个层次:什么时候重试、怎么重试、重试失败了怎么办。咱们一个一个来看。
5.1 重试策略:固定间隔、指数退避、抖动
先说说最常见的三种重试间隔策略。
5.1.1 固定间隔重试
最简单粗暴的方式。每次重试之间等待固定的时间,比如 1 秒、2 秒。
// 伪代码示例:固定间隔重试
int retryCount = 0;
int maxRetries = 3;
long interval = 1000; // 1秒
while (retryCount < maxRetries) {
try {
callRemoteService();
break; // 成功则退出
} catch (Exception e) {
retryCount++;
if (retryCount >= maxRetries) {
throw e; // 重试耗尽,抛出异常
}
Thread.sleep(interval);
}
}
这种策略的优点是简单、可预测。但缺点也很明显——如果服务正在恢复中,固定间隔可能造成「踩踏效应」。我在项目中遇到过,一个数据库连接池挂了,所有客户端都在 1 秒后同时重试,结果连接池刚缓过来又被冲垮了。
5.1.2 指数退避重试
这才是业界主流做法。每次重试的等待时间呈指数增长。比如第一次等 1 秒,第二次等 2 秒,第三次等 4 秒,第四次等 8 秒……
// 伪代码示例:指数退避
int retryCount = 0;
int maxRetries = 5;
long baseInterval = 1000; // 基础间隔 1秒
while (retryCount < maxRetries) {
try {
callRemoteService();
break;
} catch (Exception e) {
retryCount++;
if (retryCount >= maxRetries) {
throw e;
}
long waitTime = (long) Math.pow(2, retryCount) * baseInterval;
Thread.sleep(waitTime);
}
}
为什么指数退避有效?因为随着时间推移,重试频率越来越低,给下游服务留出了恢复时间。说白了,就是「别挤,一个一个来」。
5.1.3 抖动(Jitter)
指数退避虽然好,但还有一个问题:如果多个客户端同时失败,它们的退避时间完全一样,还是会同时重试。这就是「惊群效应」。
抖动的思路很简单:在退避时间上加上一个随机偏移量。
// 伪代码示例:带抖动的指数退避
long waitTime = (long) Math.pow(2, retryCount) * baseInterval;
// 加上 ±50% 的随机抖动
long jitter = (long) (waitTime * 0.5 * Math.random());
waitTime = waitTime + jitter; // 或者 waitTime - jitter
Thread.sleep(waitTime);
嗯,这里要注意:抖动不是乱加,一般控制在基础时间的 ±25% 到 ±50% 之间。加太多反而失去了退避的意义。
5.2 重试风暴防范
重试风暴,说白了就是「重试引发更多重试,最终拖垮整个系统」。我见过最惨的一次,一个核心服务挂了,所有上游服务都在疯狂重试,结果把消息队列、数据库、缓存全部打满,整个机房差点瘫痪。
怎么防范?我总结了几个关键点:
- 限制重试次数:不要无限重试。一般 3-5 次就够了。
- 限制重试窗口:比如 30 秒内最多重试 3 次,超过就不再重试。
- 熔断机制:当错误率达到阈值时,直接熔断,不再发起请求。
- 请求缓存:对于读请求,可以考虑缓存结果,避免重复请求。
5.3 幂等性设计
重试带来的最大问题是什么?是重复执行。比如支付接口,重试一次就多扣一次钱,这谁受得了?
所以,所有支持重试的接口,必须设计成幂等的。幂等的意思是:同一个请求执行多次,和执行一次的效果完全一样。
常见的幂等实现方式:
| 方案 | 说明 | 适用场景 |
|---|---|---|
| 唯一请求 ID | 客户端生成唯一 ID,服务端去重 | 写操作,如订单创建、支付 |
| 乐观锁 | 使用版本号或时间戳,防止覆盖 | 更新操作,如库存扣减 |
| 状态机 | 业务状态只能向前流转,不能回退 | 订单状态变更 |
| 去重表 | 用数据库唯一索引保证不重复 | 消息消费、事件处理 |
我曾经踩过一个坑:一个支付回调接口,没有做幂等。结果网络抖动导致回调重发了三次,用户被扣了三笔钱。虽然最后退款了,但用户体验极差,还被投诉了。从那以后,我所有写接口都强制要求幂等设计。
5.4 Spring Retry 实战
理论说完了,咱们看看实际怎么用。Spring Retry 是 Spring 生态里最常用的重试框架,用起来非常方便。
5.4.1 基础用法
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
@Service
public class PaymentService {
@Retryable(
value = {RemoteException.class, TimeoutException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 1000, multiplier = 2, maxDelay = 10000)
)
public void processPayment(String orderId) {
// 调用远程支付服务
remotePaymentService.pay(orderId);
}
}
这里 @Backoff 的 multiplier = 2 就是指数退避,delay = 1000 是初始等待 1 秒,maxDelay = 10000 是最大等待 10 秒。
5.4.2 自定义重试策略
如果内置策略不够用,可以自定义 RetryPolicy:
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.retry.backoff.ExponentialBackOffPolicy;
public RetryTemplate createRetryTemplate() {
RetryTemplate template = new RetryTemplate();
// 重试策略:最多重试 5 次
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
retryPolicy.setMaxAttempts(5);
template.setRetryPolicy(retryPolicy);
// 退避策略:指数退避 + 抖动
ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
backOffPolicy.setInitialInterval(1000);
backOffPolicy.setMultiplier(2.0);
backOffPolicy.setMaxInterval(30000);
template.setBackOffPolicy(backOffPolicy);
return template;
}
5.4.3 重试耗尽后的处理
重试次数用完了怎么办?用 @Recover 注解定义兜底逻辑:
@Retryable(
value = {RemoteException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 1000)
)
public String callRemote(String param) {
return remoteService.call(param);
}
@Recover
public String recover(RemoteException e, String param) {
// 重试耗尽后的兜底处理
log.error("远程调用失败,参数:{}", param, e);
return "fallback_result";
}
@Recover 方法里记录详细的错误日志,并发送告警。这样即使重试失败,也能第一时间发现问题。
5.5 知识体系总览
最后,我用一张图把本章的核心逻辑串起来。你一看就明白了:
这张图把重试策略、风暴防范、幂等性设计和 Spring Retry 实战串在了一起。你从「重试策略」出发,选择合适的方式,同时做好风暴防范和幂等设计,最后用 Spring Retry 落地。环环相扣,缺一不可。
好了,这一章就到这里。记住:重试不是银弹,用得好是救星,用不好是灾难。下次遇到重试场景,先想想这三个问题——重试策略选对了吗?风暴防范做了吗?接口幂等了吗?
公众号:蓝海资料掘金营,微信deep3321