今天,我好兄弟小强告诉我,老板需要他弄一个请求重试的功能
我思考了半天,没有任何的思路,简直就是本领恐慌
我应该好好学习,树立一个很好的形象
星期日,一起床,我就开始思考这个问题,思路是这样的:
- 创建一个字段(time)用于存储重试的次数
- 使用try catch来捕获异常,如果没有出现异常,说明请求成功;如果存在异常time++,并且重新调用请求
有了基本思路,那么打开IDEA将思路变成现实
- 创建UserService服务
@Service
public class UserService {
public User selectUser(){
User user = new User();
user.setName("Mic");
user.setAge(20);
//模拟网络异常
throw new RuntimeException("网络异常");
}
}
-
创建RemoteService服务,用于从UserService中获取数据
@Slf4j @Service public class RemoteService { @Autowired private UserService userService; //记录当前重试次数 private Integer time = 0; //最大重试次数 private static final Integer MAX_RETRY = 3; public User queryUser(){ User user = null; try{ log.info("正在查询用户信息"); //调用查询用户信息服务 user = userService.selectUser(); }catch(Exception e){ //如果遇到异常,进行请求重试,并将当前重试次数+1 time++; log.info("第{}次重试请求",time); //如果重试次数小于MAX_RETRY值,那么就继续查询 if(time < MAX_RETRY){ user = queryUser(); //如果重试次数大于MAX_RETRY值,退出请求,并且提示用户重试请求超过最大值 }else{ log.error("重试请求次数超过最大值,message:{}",e.getMessage()); } } return user; } }
-
创建UserController
@RestController public class UserController { @Autowired private RemoteService remoteService; @GetMapping("/queryUser") public User queryUser(){ User user = remoteService.queryUser(); return user; } }
-
测试
2022-02-20 14:42:53.111 INFO 1400 --- [nio-8080-exec-1] com.gofly.demo1.RemoteService : 正在查询用户信息 2022-02-20 14:42:53.111 INFO 1400 --- [nio-8080-exec-1] com.gofly.demo1.RemoteService : 第1次重试请求 2022-02-20 14:42:53.112 INFO 1400 --- [nio-8080-exec-1] com.gofly.demo1.RemoteService : 正在查询用户信息 2022-02-20 14:42:53.112 INFO 1400 --- [nio-8080-exec-1] com.gofly.demo1.RemoteService : 第2次重试请求 2022-02-20 14:42:53.112 INFO 1400 --- [nio-8080-exec-1] com.gofly.demo1.RemoteService : 正在查询用户信息 2022-02-20 14:42:53.112 INFO 1400 --- [nio-8080-exec-1] com.gofly.demo1.RemoteService : 第3次重试请求 2022-02-20 14:42:53.112 ERROR 1400 --- [nio-8080-exec-1] com.gofly.demo1.RemoteService : 重试请求次数超过最大值,message:网络异常
刚准备使用iMessage告诉小强,我已经解决了这个问题,想要炫耀一下
突然想到,如果写每一个Service方法,都需要加入各种if try catch,不是要写疯了
那么,我们现在需要做的就是将各种判断条件抽取出来,让所有的方法都进行重试
思考了再三,还有一个问题,有些方法我们并不希望重试,比如:登录操作
因为我们在登录的时候,如果账号或者密码错误,我们一般会选择使用异常的方式,如果我们将所有的方法都进行重试,那么会导致资源浪费
最终,使用注解的方式吧!
- 创建**@Retryable**注解,在需要进行重试的方法上增加该注解
- 实现拦截,拦截被**@Retryable**标识的方法
忙活了一下午,终于弄出来了
- 创建自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Retryable {
int maxAttemps() default 0;
}
- 创建RetryService
@Slf4j
@Service
public class RetryableService {
@Autowired
private UserService userService;
//自定义注解
@Retryable(maxAttemps = 5)
public User queryUser(){
log.info("正在查询用户信息");
User user = userService.selectUser();
return user;
}
}
- 创建Aspect
@Component
@Slf4j
@Aspect
public class RetryableAsppect {
//记录当前重试次数
private Integer time = 0;
@Pointcut("@annotation(com.gofly.demo2.Retryable)")
public void pointCut(){}
@Around("pointCut()")
public Object retryHandler(ProceedingJoinPoint joinPoint) throws Throwable {
//获取方法签名
MethodSignature signature = (MethodSignature)joinPoint.getSignature();
//获取方法
Method method = signature.getMethod();
Object obj = null;
//获取注解
Retryable retryable = method.getAnnotation(Retryable.class);
//如果没有被注解标识,直接放行
if(retryable == null){
joinPoint.proceed();
}else{
int maxAttemps = retryable.maxAttemps();
try{
obj = joinPoint.proceed();
}catch (Exception e){
time++;
//如果当前重试次数大于最大值
if(time > maxAttemps){
log.error("重试次数大于最大值");
}else{
log.info("进行第{}次请求重试",time);
//进行重试
this.retryHandler(joinPoint);
}
}
}
return obj;
}
}
我就这样愉快的和小强开个视频,炫耀一下我的成果
但是,小强又给我增加需求,需要设置多长时间重试,比如我设置2ms,重试次数2次,就需要每2ms重试一次,总共2次
真的头大!
间隔时间?可以使用Thread.sleep()?
就当一筹莫展的时候,GitHub上找到了一个spring-retry,小强需要的功能不就是这个框架实现的功能吗!
在Github看了文档之后,我第一个感觉就是特别像Hystrix
spring-retry解决什么样的问题呢?
解决调用远程服务时,会因为网络抖动、服务不稳定等问题导致请求失败,这个问题的发生有可能是随机的,下次请求的时候,有可能访问会成功
所以,我们假设在max的范围之内,多次进行请求
如果超过最大重试次数,那么就提示用户请求错误
文档地址:https://github.com/spring-projects/spring-retry
我们来看看具体是应该怎么实现小强的需求:
- 如果第三方服务请求失败,那么就重试三次
- 每一次间隔2秒
- 重试的条件:只有出现RuntimeException的时候才重试
- 重试三次以后还是失败的话,就将第三方服务提示的内容,返回给用户
使用Spring-Retry so easy!
- 依赖
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
- **创建SpringRetryService:**用于请求第三方服务
@Retryable(
value = RuntimeException.class,
maxAttempts = 3,
backoff = @Backoff(delay = 2000L)
)
public User queryUser(){
User user = userService.selectUser();
return user;
}
//如果根据上面的策略都失败,就需要调用回退机制,这里就和Hystrix一样的
/**
* 兜底处理方式
* TODO:要求返回参数要和queryUser一致
* @param e
* @return
*/
@Recover
public User msg(Throwable e){
log.error("错误提示:{}",e.getMessage());
User user = new User();
return user;
}
这里还有一个内容,一定要在启动类增加**@EnableRetry**
- 测试
2022-02-20 21:10:45.218 INFO 5522 --- [nio-8080-exec-1] com.gofly.common.UserService : 正在调用UserService用户查询服务,当前时间:Sun Feb 20 21:10:45 CST 2022
2022-02-20 21:10:47.226 INFO 5522 --- [nio-8080-exec-1] com.gofly.common.UserService : 正在调用UserService用户查询服务,当前时间:Sun Feb 20 21:10:47 CST 2022
2022-02-20 21:10:49.233 INFO 5522 --- [nio-8080-exec-1] com.gofly.common.UserService : 正在调用UserService用户查询服务,当前时间:Sun Feb 20 21:10:49 CST 2022
2022-02-20 21:10:49.236 ERROR 5522 --- [nio-8080-exec-1] com.gofly.demo3.SpringRetryService : 错误提示:网络异常
上面我们只是简单的实现了基本的功能,现在我们整理一下retry的知识点
- @EnableRetry
是否开启重试
- @Retryable
value :默认为空,当参数exclude也为空时,所有的Exception都需要重试,比如制定RuntimeException,就只有RuntimeException异常才需要重试
include :需要进行重试的异常,默认为空。
exclude :不需要重试的异常。默认为空。
stateful 标记以表示重试是有状态的:即重新引发异常,但重试策略使用相同的策略应用于具有相同参数的后续调用。如果为 false,则不会重新引发可重试的异常。如果重试是有状态的,则为 true,默认值为 false
maxAttempts :最大重试次数,默认为3
backoff :回避策略,默认为空。该参数为空时是,失败立即重试,比如设定每一次请求的重试间隔时间,使用线程休眠的方式
exceptionExpression:指定在 SimpleRetryPolicy.canRetry() 返回 true ,可用于有条件地禁止重试后要计算的表达式,仅在引发异常后调用
- @Backoff
回退策略,立即重试还是需要有间隔
value:功能和delay()一样。当 delay() 不为零时,将忽略此元素的值,否则将取此元素的值,以毫秒为单位的延迟(默认值 1000)
delay:和value()一样。
maxDelay:重试之间的最长等待时间(以毫秒为单位)。如果小于delay(则默认值为 DEFAULT_MAX_INTERVAL = 30000L,重试之间的最大延迟(默认值 0 = )
multiplier:如果为正数,则用作乘数,用于生成回退的下一个延迟。乘法器用于计算下一个回退延迟(默认值为 0)
random:是否启用随机退避策略,默认false。在指数情况下(multiplier()> 0),将此值设置为 true 以使回退延迟随机化,以便maxDelay乘以delay,并且两个值之间的分布是均匀的。
- @Recover
是一个兜底的方法,当@Retryable失败之后,调用该方法
注意:@Recover和@Retryable的返回值需要一样
- 断路器**@CircuitBreaker**
配合**@Recover**一起使用
include: 指定处理的异常类。默认为空
exclude: 指定不需要处理的异常。默认为空
vaue: 指定要重试的异常。默认为空
maxAttempts: 最大重试次数。默认3次
openTimeout: 配置熔断器打开的超时时间,默认5s,当超过openTimeout之后熔断器电路变成半打开状态(只要有一次重试成功,则闭合电路)
resetTimeout: 配置熔断器重新闭合的超时时间,默认20s,超过这个时间断路器关闭
收工,小强完成了领导的任务,我也趁机炫耀了一把!