Springboot 整合Retry 实现重试机制
背景:项目中使用restTemplate进行服务间的接口调用,有次报了SocketTimeoutException的异常,由于网络波动导致超时没有接收到响应报文,由此引发了我对RestTemplate的默认重试策略及底层配置的研究。
restTemplate默认重试三次,请求发送成功不开启重试。后续可能会写一篇关于restTemplate重试策略的博文,此篇主要写Springboot整合Retry实现重试机制。现在让我们进入主题吧。
Retry的优势:
Retry重试框架,支持AOP切入的方式使用,而且能使用注解;重试次数、重试延迟、重试触发条件、重试的回调方法等等我们都能很轻松结合注解以一种类似配置参数的方式去实现,优雅无疑。
- 导入依赖
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
</dependency>
<!--若导入了spring-boot-starter-aop的包可不导入aspectjweaver的包,已经包含了-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
- 开启重试
在启动类上添加@EnableRetry注解
@SpringBootApplication
@EnableRetry
public class DemoApplication{}
- 测试demo
@Service
@Slf4j
public class TestService {
@Resource
RestTemplate longTimeRestTemplate;
@Retryable(value = {ResourceAccessException.class},maxAttempts = 3,backoff = @Backoff(delay = 1000,multiplier = 1.5))
public void retry() {
log.info("进入retry测试方法");
//restTemplate调用的连接随便写的,是个无效的连接,会抛出ResourceAccessException异常
longTimeRestTemplate.getForEntity("http://localhost:8088/test", String.class, String.class);
}
结果:
2021-05-2717:03:00.925 INFO[http-nio-8082-exec-1][com.test.verification.service.impl.TestService:20] 进入retry测试方法2021-05-2717:03:06.056 INFO[http-nio-8082-exec-1][com.test.verification.service.impl.TestService:20] 进入retry测试方法2021-05-2717:03:11.087 INFO[http-nio-8082-exec-1][com.test.verification.service.impl.TestService:20] 进入retry测试方法2021-05-2717:03:15.122 WARN[http-nio-8082-exec-1][com.test.common.handler.GlobalExceptionHandler:46][exceptionHandler] Throwable-warn :I/O error on GET requestfor"http://localhost:8088/test": Connect to localhost:8088[localhost/127.0.0.1, localhost/0:0:0:0:0:0:0:1] failed: Connection refused: connect; nested exception is org.apache.http.conn.HttpHostConnectException: Connect to localhost:8088[localhost/127.0.0.1, localhost/0:0:0:0:0:0:0:1] failed: Connection refused: connect
org.springframework.web.client.ResourceAccessException: I/O error on GET requestfor"http://localhost:8088/test": Connect to localhost:8088[localhost/127.0.0.1, localhost/0:0:0:0:0:0:0:1] failed: Connection refused: connect; nested exception is org.apache.http.conn.HttpHostConnectException: Connect to localhost:8088[localhost/127.0.0.1, localhost/0:0:0:0:0:0:0:1] failed: Connection refused: connect
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:748)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:674)
at org.springframework.web.client.RestTemplate.getForEntity(RestTemplate.java:342)
at com.test.verification.service.impl.TestService.testOne(TestService.java:21)
at com.test.verification.service.impl.TestService$$FastClassBySpringCGLIB$$8975be93.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:771)
@Retryable
- value:指定发生的异常进行重试,可以指定多个异常或者Exception.class所有异常
- include:和value一样,默认空,当exclude也为空时,所有异常都重试
- exclude:指定异常不重试,默认空,当include也为空时,所有异常都重试
- maxAttemps:重试次数,默认3(包括第一次调用,也就是说如果设置3次,调用一次后,如果一直失败触发重试,那么还当前方法还会调用2次);
- backoff:重试补偿机制,默认使用@Backoff注解
@Backoff注解
- value:返回固定多少毫秒后重试(默认为1000毫秒)
- delay:返回延迟多少毫秒后重试(默认为0毫秒)
- multiplier:指定延迟的倍数,比如delay=5000l,multiplier=2时,第一次重试为5秒,第二次=上次延迟5秒乘以2倍10秒,第三次为上次延迟10秒乘以两倍20秒(
上次延迟秒数乘以倍数
) - value与delay设置一个即可。若有multiplier参数,value与deplay都会与multiplier结合使用
@Recover
当重试到达指定次数时,被注解的方法将被回调,可以在该方法中进行日志处理。需要注意的是发生的异常和入参类型一致时才会回调。
若retrable方法中抛出了多个异常,Recover就需要写多个,或者recover中异常为Exception可以拦截所有异常回调
@Service
@Slf4j
public class TestService{
@Resource
RestTemplate longTimeRestTemplate;
@Retryable(value={ResourceAccessException.class},maxAttempts=3,backoff= @Backoff(delay=1000,multiplier=1.5))
public void retry(String name){
log.info("进入retry测试方法");
longTimeRestTemplate.getForEntity("http://localhost:8088/test", String.class, String.class);}
//第一个参数为retryable抛出的异常,后面的参数为Retryable注解方法的入参
@Recover
public void recover(ResourceAccessException e,String name){
System.out.println("回调方法执行!!!!");
//记日志到数据库 或者调用其余的方法}}
结果:
2021-05-2811:09:46.765 INFO[http-nio-8082-exec-1][com.test.verification.service.impl.TestService:22] 进入retry测试方法2021-05-2811:09:51.956 INFO[http-nio-8082-exec-1][com.test.verification.service.impl.TestService:22] 进入retry测试方法2021-05-2811:09:57.486 INFO[http-nio-8082-exec-1][com.test.verification.service.impl.TestService:22] 进入retry测试方法
回调方法执行!!!!
- Recover使用注意项
1:Recover返回值必须与retry方法的返回值一致,否则不会调用到recover方法且会抛出ExhaustedRetryException异常。
2:Recover方法的第一入参为要重试的异常必须与retry方法抛出的异常相同(或者为异常父类),其他参数与@Retryable保持一致
org.springframework.retry.ExhaustedRetryException: Cannotlocate recovery method; nested exception is org.springframework.web.client.ResourceAccessException: I/O error on GET requestfor"http://localhost:8088/test": Connect to localhost:8088[localhost/127.0.0.1, localhost/0:0:0:0:0:0:0:1] failed: Connection refused: connect; nested exception is org.apache.http.conn.HttpHostConnectException: Connect to localhost:8088[localhost/127.0.0.1, localhost/0:0:0:0:0:0:0:1] failed: Connection refused: connect
at org.springframework.retry.annotation.RecoverAnnotationRecoveryHandler.recover(RecoverAnnotationRecoveryHandler.java:70)
at org.springframework.retry.interceptor.RetryOperationsInterceptor$ItemRecovererCallback.recover(RetryOperationsInterceptor.java:142)
at org.springframework.retry.support.RetryTemplate.handleRetryExhausted(RetryTemplate.java:539)
at org.springframework.retry.support.RetryTemplate.doExecute(RetryTemplate.java:387)
Retry失效?
@Service
@Slf4j
public class TestService{
@Resource
RestTemplate longTimeRestTemplate;
public voidtestOne(){
log.info("进入testOne测试方法");
test();}
@Retryable(value= ResourceAccessException.class,maxAttempts=3,backoff= @Backoff(delay=1000,multiplier=1.5))
public voidtest(){
log.info("进入testOne测试方法");
longTimeRestTemplate.getForEntity("http://localhost:8088/test", String.class, String.class);}}
若服务调用testOne, testOne又调用test方法,在test方法上的Retry注解会失效!!!
这是因为retry用到了aspect增强,在方法内部调用的时候,会使aspect增强失效,那么retry当然也会失效。
具体原理可参考:https://blog.csdn.net/u013151053/article/details/106124048/
抛出ExhaustedRetryException: Cannot locate recovery method; nested exception is ?
当Retryable方法中抛出多个异常,Recover方法只捕获一个异常的话,其他异常没有被捕获会抛出ExhaustedRetryException
示例:
@Service
@Slf4j
public class TestService{
@Resource
RestTemplate longTimeRestTemplate;
@Retryable(value={ResourceAccessException.class},maxAttempts=3,backoff= @Backoff(delay=1000,multiplier=1.5))
public void retry(String name){
log.info("进入retry测试方法");
try{
longTimeRestTemplate.getForEntity("http://localhost:8088/test", String.class, String.class);}catch(ResourceAccessException e){
throw e;}
throw new BusinessException(100,"测试失败");}
//第一个参数为retryable抛出的异常,后面的参数为Retryable注解方法的入参
@Recover
public void recover(ResourceAccessException e,String name){
System.out.println("回调方法执行!!!!");
//记日志到数据库 或者调用其余的方法}}
说明:若Retryable方法中抛出了多个异常,那么就需要有多个Recover方法或者一个Recover方法拦截Exception所有异常,否则会抛出ExhaustedRetryException异常
修改方案:
方案一:
添加其他的异常
@Recover
public void recover(BusinessException e,String name){
System.out.println("回调方法执行!!!!");
//记日志到数据库 或者调用其余的方法}
方案二:
添加所有异常
@Recover
public void recover(Exception e,String name){
System.out.println("回调方法执行!!!!");
//记日志到数据库 或者调用其余的方法}