Springboot 整合Retry 实现重试机制

背景

背景:项目中使用restTemplate进行服务间的接口调用,有次报了SocketTimeoutException的异常,由于网络波动导致超时没有接收到响应报文,由此引发了我对RestTemplate的默认重试策略及底层配置的研究。
restTemplate默认重试三次,请求“发送成功”不开启重试。后续可能会写一篇关于restTemplate重试策略的博文,此篇主要写Springboot整合Retry实现重试机制。现在让我们进入主题吧。

Retry的优势

Retry重试框架,支持AOP切入的方式使用,而且能使用注解;重试次数、重试延迟、重试触发条件、重试的回调方法等等我们都能很轻松结合注解以一种类似配置参数的方式去实现,优雅无疑。

思路

使用@Retryable和@Recover实现重处理,以及重处理失后的回调

实现

1.导入依赖

<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>

2.开启重试
在启动类上添加@EnableRetry注解

@SpringBootApplication
@EnableRetry
public class DemoApplication {
}

3.在指定方法上标记@Retryable来开启重试
测试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-27 17:03:00.925 INFO  [http-nio-8082-exec-1] [com.test.verification.service.impl.TestService:20] 进入retry测试方法
2021-05-27 17:03:06.056 INFO  [http-nio-8082-exec-1] [com.test.verification.service.impl.TestService:20] 进入retry测试方法
2021-05-27 17:03:11.087 INFO  [http-nio-8082-exec-1] [com.test.verification.service.impl.TestService:20] 进入retry测试方法
2021-05-27 17:03:15.122 WARN  [http-nio-8082-exec-1] [com.test.common.handler.GlobalExceptionHandler:46] [exceptionHandler] Throwable-warn :I/O error on GET request for "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 request for "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可以拦截所有异常回调
4.在指定方法上标记@Recover来开启重试失败后调用的方法(注意,需跟重处理方法在同一个类中)

@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-28 11:09:46.765 INFO  [http-nio-8082-exec-1] [com.test.verification.service.impl.TestService:22] 进入retry测试方法
2021-05-28 11:09:51.956 INFO  [http-nio-8082-exec-1] [com.test.verification.service.impl.TestService:22] 进入retry测试方法
2021-05-28 11: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: Cannot locate recovery method; nested exception is org.springframework.web.client.ResourceAccessException: I/O error on GET request for "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 void testOne(){
        log.info("进入testOne测试方法");
        test();
    }
    @Retryable(value = ResourceAccessException.class,maxAttempts = 3,backoff = @Backoff(delay = 1000,multiplier = 1.5))
    public void test(){
        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("回调方法执行!!!!");
     //记日志到数据库 或者调用其余的方法
 }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

五月天的尾巴

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值