1. 开发中接口重试的场景
日常开发中,经常会遇到这样的场景:执行一次接口调用,如RPC调用,偶现失败,原因可能是dubbo超时、连接数耗尽、http网络抖动等,出现异常时我们并不能立即知道原因并作出反应,可能只是一个普通的RpcException或RuntimeException,
对于这种小概率的异常,往往需要尝试再次调用(前提是接口是幂等的),因为由于网络问题、下游服务暂时的不稳定导致的异常,一段时间后理论上是可以自恢复的;
例如,有时候项目需要进行同步数据,一定要同步成功,不然对于业务会有影响,偶发性的会出现调用接口失败,失败并不是特别多;
一般我们处理偶发异常的流程如下:异常时,
(1)循环的进行远程调用若干次数,记录一下调用失败的记录;
(2)休眠一段时间,尝试等待下游系统自恢复或释放连接数,继续循环调用失败的请求;
(3)如果再调用失败、通过人工二次调用进行修复;
当然,你可以通过写一个指定次数的for循环来执行重试,或者在catch时再次尝试调用一次接口,这么做是可以,但是代码会显得不够优雅;通过前面描述的2种方式,思考下:
对于一个重试模块,要具备哪些属性?
(1)重试时执行的方法,这个方法执行成功后就不用再重试,这个方法要求幂等
,并且失败时需要抛出异常来标识执行失败;
(2)重试策略:重试最大次数、重试最大时间即XXms后不再重试、重试次数之间的间隔、对于哪些异常才需要重试;
(3)重试次数达到上限任未成功,最后的兜底逻辑,如插入重试任务表、发送消息给系统管理员等;
2. 重试框架需要解决的问题
-
重试的策略(
RetryPolicy
)
无限重试?最多重试几次、指定的时间范围内可以重试、或者多种重试策略组合。 -
重试的要休眠多久(
BackOffPolicy
)
重试时间间隔,每次都休眠固定的时间、第一次1s 第二次2s 第三次4s 、随机的休眠时间 -
兜底方案(
Recover
)
如果所有的重试都失败了、兜底方案是什么?有点类似限流,最差返回你系统繁忙的界面。
3. Spring Retry的基本概念
Spring Retry 是从 Spring batch 中独立出来的一个功能,主要实现了重试和熔断,对于那些重试后不会改变结果,毫无意义的操作,不建议使用重试。spring retry提供了注解和编程 两种支持,提供了 RetryTemplate 支持,类似RestTemplate。整个流程如下:
结合我们上面对一个重试模块应该具备哪些属性的思考,看看这几个属性的作用:
-
RetryTemplate
: 封装了Retry基本操作,是进入spring-retry框架的整体流程入口,通RetryTemplate可以指定监听、回退策略、重试策略等。 -
RetryCallback
:该接口封装了业务代码,且failback后,会再次调用RetryCallback接口,直到达到重试次数/时间上限; -
RecoveryCallback
:当RetryCallback不能再重试的时候,如果定义了RecoveryCallback,就会调用RecoveryCallback,并以其返回结果作为最终的返回结果。此外,RetryCallback和RecoverCallback定义的接口方法都可以接收一个RetryContext上下文参数,通过它可以获取到尝试次数、异常,也可以通过其setAttribute()和getAttribute()来传递一些信息。 -
RetryPolicy
:重试策略,描述什么条件下可以尝试重复调用RetryCallback接口;策略包括最大重试次数、指定异常集合/忽略异常集合、重试允许的最大超时时间;RetryTemplate内部默认时候用的是SimpleRetryPolicy,SimpleRetryPolicy默认将对所有异常进行尝试,最多尝试3次。还有其他多种更为复杂功能更多的重试策略; -
BackOffPolicy
:回退策略,用来定义在两次尝试之间需要间隔的时间,如固定时间间隔、递增间隔、随机间隔等;RetryTemplate内部默认使用的是NoBackOffPolicy,其在两次尝试之间不会进行任何的停顿。对于一般可重试的操作往往是基于网络进行的远程请求,它可能由于网络波动暂时不可用,如果立马进行重试它可能还是不可用,但是停顿一下,过一会再试可能它又恢复正常了,所以在RetryTemplate中使用BackOffPolicy往往是很有必要的; -
RetryListener
:RetryTemplate中可以注册一些RetryListener,可以理解为是对重试过程中的一个增强,它可以在整个Retry前、整个Retry后和每次Retry失败时进行一些操作;如果只想关注RetryListener的某些方法,则可以选择继承RetryListenerSupport,它默认实现了RetryListener的所有方法;
4. 代码示例
实际开发中,我们一般使用简单的固定间隔和重试指定次数的策略,代码示例如下:
2.1 添加依赖
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
2.2 配置文件
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
builder.setConnectTimeout(Duration.ofMillis(5000));
builder.setReadTimeout(Duration.ofMillis(5000));
RestTemplate restTemplate = builder.build();
return restTemplate;
}
@Bean
public RetryTemplate retryTemplate() {
RetryTemplate retryTemplate = new RetryTemplate();
// 设置退避策略:固定间隔时间重试
retryTemplate.setBackOffPolicy(new FixedBackOffPolicy());
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
// 配置的重试次数
retryPolicy.setMaxAttempts(3);
retryTemplate.setRetryPolicy(retryPolicy);
return retryTemplate;
}
}
@Autowired
private RestTemplate restTemplate;
@Autowired
private RetryTemplate retryTemplate;
/**
* 重试请求
*
* @param url
* @param object
* @return
*/
private CommonResponse retryRequestTemplate(String url, Object object) {
CommonResponse commonResponse = retryTemplate.execute(
retryContext ->
restTemplate.postForObject(url, object, CommonResponse.class),
retryContext -> {
log.error("多次调用{}接口失败,retryContext={}", url, retryContext);
return null;
});
return commonResponse;
}