0
点赞
收藏
分享

微信扫一扫

好兄弟给我出了一个难题:重试机制

大南瓜鸭 2022-02-20 阅读 92

在这里插入图片描述

今天,我好兄弟小强告诉我,老板需要他弄一个请求重试的功能

我思考了半天,没有任何的思路,简直就是本领恐慌

我应该好好学习,树立一个很好的形象

星期日,一起床,我就开始思考这个问题,思路是这样的:

  • 创建一个字段(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,超过这个时间断路器关闭

在这里插入图片描述

收工,小强完成了领导的任务,我也趁机炫耀了一把!

举报

相关推荐

0 条评论