
工作里总遇到需要重试的场景,比如调用第三方接口超时、数据库临时连接失败之类的情况。以前我都是用 try...catch 加 while 循环写,代码又丑又难维护,后来发现了 Spring Retry,用起来是真方便,分享下我的使用经验。
先搞定依赖
用 Spring Retry 第一步肯定要引依赖,我这边是 Spring Boot 项目,除了核心的 spring-retry,还得加 aop 依赖,不然注解不好使。
xml
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
两种使用方式,按需选择
Spring Retry 有命令式和声明式两种用法,我平时更爱用声明式,注解写起来省事,但有时候复杂场景还得靠命令式。
命令式:灵活控制细节
命令式主要靠 RetryTemplate,能手动配置重试次数、间隔、触发条件这些。我认为这种方式适合需要动态调整重试规则的场景,比如根据不同业务参数改变重试策略。
简单示例安排上,最多重试 3 次,每次间隔 1 秒,只有抛 RemoteAccessException 才重试:
java
RetryTemplate template = RetryTemplate.builder()
.maxAttempts(3)
.fixedBackoff(1000)
.retryOn(RemoteAccessException.class)
.build();
// 执行业务逻辑
template.execute(ctx -> {
// 这里放可能失败的操作,比如调用接口
System.out.println("执行业务逻辑,当前重试次数:" + ctx.getRetryCount());
return "处理结果";
});
还有些实用的配置,比如指数退避,第一次间隔 100ms,之后每次翻倍,最大间隔 10 秒,适合怕频繁请求压垮服务的场景:
java
RetryTemplate.builder()
.maxAttempts(10)
.exponentialBackoff(100, 2, 10000)
.retryOn(IOException.class)
.build();
如果想无限重试,只要间隔合理也可以,比如 1-3 秒随机间隔:
java
RetryTemplate.builder()
.infiniteRetry()
.retryOn(IOException.class)
.uniformRandomBackoff(1000, 3000)
.build();
重试耗尽还没成功怎么办?可以加个兜底逻辑,用 RecoveryCallback:
java
template.execute(new RetryCallback<Object, Throwable>() {
@Override
public Object doWithRetry(RetryContext context) throws Throwable {
System.out.println("尝试执行,次数:" + context.getRetryCount());
// 模拟失败
throw new RemoteAccessException("调用失败");
}
}, new RecoveryCallback<Object>() {
@Override
public Object recover(RetryContext context) throws Exception {
System.out.println("重试都失败了,执行兜底方案");
return "兜底返回结果";
}
});
声明式:注解搞定一切
声明式用注解,代码更简洁,我平时写常规业务都用这个。首先得在启动类加 @EnableRetry 开启功能:
java
@EnableRetry
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
然后在需要重试的方法上加 @Retryable,比如这个方法,抛 RuntimeException 时重试,最多 5 次,每次间隔 1 秒,之后每次乘以 1.5:
java
@Service
public class MyService {
@Retryable(value = RuntimeException.class, maxAttempts = 5, backoff = @Backoff(value = 1000L, multiplier = 1.5))
public void sayHello() {
System.out.println("执行 sayHello,时间:" + LocalDateTime.now());
// 模拟失败场景
throw new RuntimeException("测试重试");
}
}
也能指定多个异常类型,还能加监听器:
java
@Retryable(value = {IOException.class, RemoteAccessException.class},
listeners = {"myRetryListener"},
maxAttempts = 5, backoff = @Backoff(delay = 100, maxDelay = 500))
public void sayHi() {
// 业务逻辑
}
重试失败后的兜底方法用 @Recover 注解,得和重试方法在同一个类里,返回类型要一致,还能拿到原始参数:
java
@Recover
public void recoverRuntime(RuntimeException e) {
System.out.println("RuntimeException 重试失败,兜底处理:" + e.getMessage());
}
// 带参数的兜底方法
@Retryable(value = RemoteAccessException.class)
public void service(String str1, String str2) {
throw new RemoteAccessException("调用失败");
}
@Recover
public void recoverRemote(RemoteAccessException e, String str1, String str2) {
System.out.println("RemoteAccessException 重试失败,参数:" + str1 + "," + str2);
}
重试策略和监听器
重试策略
在我看来,重试策略就是控制"什么时候重试""重试多少次"的规则。Spring Retry 有现成的实现,比如 SimpleRetryPolicy 控制最大重试次数:
java
// 包括首次尝试,最多 5 次,所有 Exception 都重试
SimpleRetryPolicy policy = new SimpleRetryPolicy(5, Collections.singletonMap(Exception.class, true));
RetryTemplate template = new RetryTemplate();
template.setRetryPolicy(policy);
还有 TimeoutRetryPolicy,控制超时时间,30 秒内可以一直重试:
java
TimeoutRetryPolicy policy = new TimeoutRetryPolicy();
policy.setTimeout(30000L);
template.setRetryPolicy(policy);
监听器
想监控重试过程的话,就用 RetryListener,能拿到重试开始、出错、结束的事件。我们的经验是,用监听器记录重试日志很方便,排查问题时能清楚看到每一次重试的情况:
java
RetryListener myListener = new RetryListener() {
@Override
public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
System.out.println("重试开始");
return true;
}
@Override
public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
System.out.println("重试出错,次数:" + context.getRetryCount() + ",异常:" + throwable.getMessage());
}
@Override
public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
System.out.println("重试结束,是否成功:" + (throwable == null));
}
};
// 注册监听器
template.registerListener(myListener);
无状态和有状态重试
这个点容易被忽略,我简单说下。无状态重试就是普通场景,重试上下文存在线程栈里,不用额外存储,大部分情况都能用。
有状态重试就特殊了,比如数据库事务场景,失败后需要回滚事务再重新开始,这时候上下文得存在堆里,避免丢失。Spring Retry 用 RetryContextCache 来存储,默认是内存 Map,不够用的话可以自己定制。
一些使用小技巧
- 不要滥用重试,像参数错误这种明确不会成功的异常,千万别加重试,不然只会浪费资源。
- 间隔时间要合理,远程调用可以设长点,本地操作可以短点,也能用上随机间隔,避免同时重试造成峰值。
- 兜底方法很重要,一定要处理好重试失败的情况,比如返回默认值、触发告警之类的。
- 用表达式配置更灵活,能从配置文件或者其他 Bean 里读取参数,不用写死代码:
java
@Retryable(maxAttemptsExpression = "@runtimeConfigs.maxAttempts",
backoff = @Backoff(delayExpression = "@runtimeConfigs.initial"))
public void service() {
// 业务逻辑
}
总的来说,Spring Retry 把重试逻辑抽象得很好,不用我们自己写重复代码,不管是简单场景还是复杂场景都能覆盖。我用下来感觉是真的香,推荐大家试试!