有些场景需要我们对一些异常情况下面的任务进行重试,比如:调用远程的RPC/RestTemplate或者Feign服务,可能由于网络抖动出现第一次调用失败,尝试几次就可以恢复正常。当然调用内部的其他服务也会遇到调用失败的情况,这时候就需要通过一些方法来进行重试,比如通过while循环手动重复调用或是通过JDK/CGLib动态代理的方式来进行重试。但是这种方法比较笨重,且对原有逻辑代码的侵入性比较大。
Spring已经为我们提供了封装好的重试功能,spring-retry是spring提供的一个重试框架,使我们可以通过@Retryable
和@Recover
注解来完成重试和重试失败后的回调。
一、Spring Retry配置
POM引入依赖:
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
当不清楚引入依赖最新的版本和groupId的时候,也可以在IDEA中通过它的提示快速添加:二、启动类
在Spring Boot 应用入口启动类,也就是配置类的上面加上@EnableRetry
注解,表示让重试机制生效。
三、编写Controller
简单的Controller,其注入RestTemplate来调用其他服务接口 。代码中被调用的http://www.guo.com:8080/v5/packageIndex/findByState/60
服务没启动,所以会抛出404异常,是为了触发重试机制。
@RestController
@Slf4j
@RequestMapping(value = "/rest")
public class RestTemplateController {
@Autowired
private RestTemplate restTemplate;
@RequestMapping(value = "/restTemplate")
@Retryable(value = RestClientException.class, maxAttempts = 3,
backoff = @Backoff(delay = 5000L, multiplier = 2))
public JsonResult<String> findStateByStateFromPackageService() {
log.info("发起远程API请求");
String response = null;
response = restTemplate.getForObject("http://www.guo.com:8080/v5/packageIndex/findByState/60", String.class);
log.info("Rest请求数据:" + response);
return JsonResult.of(response, true, "成功调用");
}
}
- @Retryable注解的方法在发生异常时会重试,参数说明:
value:当指定异常发生时会进行重试 ,HttpClientErrorException是RestClientException的子类。如果所有异常都进行重试,改成Exception.class
。
include:和value一样,默认空。如果 exclude也为空时,所有异常都重试
exclude:指定异常不重试,默认空。如果 include也为空时,所有异常都重试
maxAttemps:最大重试次数,默认3
backoff:重试等待策略,默认空 - @Backoff注解为重试等待的策略,参数说明:
delay:指定重试的延时时间,默认为1000毫秒
multiplier:指定延迟的倍数,比如设置delay=5000,multiplier=2时,第一次重试为5秒后,第二次为10(5x2)秒,第三次为20(10x2)秒。
四、启动服务进行测试
启动当前调用方服务后,向http://localhost:8085/rest/restTemplate发起请求,结果如下:
2020-10-04 12:23:39 [http-nio-8085-exec-1] INFO c.RestTemplateController:128 -发起远程API请求
2020-10-04 12:23:48 [http-nio-8085-exec-1] INFO c.RestTemplateController:128 -发起远程API请求
2020-10-04 12:24:02 [http-nio-8085-exec-1] INFO c.RestTemplateController:128 -发起远程API请求
2020-10-04 12:24:06 [http-nio-8085-exec-1] ERROR o.a.c.c.C.[.[localhost].[/].[dispatcherServlet]:175 -Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://localhost:8080/v5/packageIndex/findByState/60": Connect to localhost:8080 [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:8080 [localhost/127.0.0.1, localhost/0:0:0:0:0:0:0:1] failed: Connection refused: connect] with root cause
java.net.ConnectException: Connection refused: connect
at java.base/java.net.PlainSocketImpl.waitForConnect(Native Method)
从结果可以看出:
第一次请求失败之后,延迟后重试
第二次请求失败之后,延迟后重试
第三次请求失败之后,抛出异常
五、@Recover注解
当重试次数达到设置的次数的时候,还是失败抛出异常,执行@Recover
注解的回调函数。
新建一个service提供重试和recover方法。
package com.pay.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import java.time.LocalTime;
/**
* @ClassName: PayService
* @Description: 模拟库存扣减的service,实现扣减业务的可重试以及多长尝试后失败的回调处理。
* @author: 郭秀志 jbcode@126.com
* @date: 2020/10/4 12:43
* @Copyright:
*/
@Service
@Slf4j
public class PayService {
private final int totalNum = 53;
@Retryable(value = Exception.class, maxAttempts = 3, backoff = @Backoff(delay = 2000L, multiplier = 1.5))
public int minGoodsnum(int num) throws Exception {
log.info("减库存开始" + LocalTime.now());
try {
int i = 1 / 0;
} catch (Exception e) {
log.error("illegal operation");
}
if (num <= 0) {
throw new IllegalArgumentException("数量不对");
}
log.info("减库存执行结束" + LocalTime.now());
return totalNum - num;
}
@Recover
public int recover(Exception e) {
log.warn("[recover method]减库存失败!!!" + LocalTime.now());
//记日志到数据库
//发送异常的邮件通知
return totalNum;
}
}
controller增加调用上面service的逻辑
@Autowired
private PayService payService;
@GetMapping("/retry")
public String getNum() throws Exception {
int i = payService.minGoodsnum(-1);
System.out.println("====" + i);
return "succeess";
}
测试recover,访问http://localhost:8085/rest/retry,控制台打印信息:
2020-10-04 12:49:54 [http-nio-8085-exec-1] INFO PayService:27 -减库存开始12:49:54.328000400
2020-10-04 12:49:54 [http-nio-8085-exec-1] ERROR PayService:31 -illegal operation
2020-10-04 12:49:56 [http-nio-8085-exec-1] INFO PayService:27 -减库存开始12:49:56.337155100
2020-10-04 12:49:56 [http-nio-8085-exec-1] ERROR PayService:31 -illegal operation
2020-10-04 12:49:59 [http-nio-8085-exec-1] INFO PayService:27 -减库存开始12:49:59.339616100
2020-10-04 12:49:59 [http-nio-8085-exec-1] ERROR PayService:31 -illegal operation
2020-10-04 12:49:59 [http-nio-8085-exec-1] WARN PayService:42 -[recover method]减库存失败!!!12:49:59.341657600
====53