• Java 远程调用失败?如何优雅的进行重试?


    在日常开发的过程中我们经常会需要调用第三方组件或者数据库,有的时候可能会因为网络抖动或者下游服务抖动,导致我们某次查询失败。

    这种时候我们往往就会进行重试,当重试几次后依旧还是失败的话才会向上抛出异常进行失败。接下来阿粉就给大家演示一下通常是如何做的,以及如何更优雅的进行重试。

    常规做法

    我们先来看一下常规做法,常规做法首先会设置一个重试次数,然后通过 while 循环的方式进行遍历,当循环次数没有达到重试次数的时候,直到有正确结果后就返回,如果重试依旧失败则会进行睡眠一段时间,再次重试,直到正常返回或者达到重试次数返回。

    package com.example.demo.service;
    
    import org.springframework.retry.annotation.Backoff;
    import org.springframework.retry.annotation.Retryable;
    import org.springframework.stereotype.Service;
    
    import java.util.Random;
    import java.util.concurrent.TimeUnit;
    
    @Service
    public class HelloService {
      public String sayHello(String name) {
        String result = "";
        int retryTime = 3;
        while (retryTime > 0) {
          try {
            //
            result = name + doSomething();
            return result;
          } catch (Exception e) {
            System.out.println("send message failed. try again in 1's");
            retryTime--;
            try {
              TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException ex) {
              throw new RuntimeException(ex);
            }
          }
        }
        return result;
      }
    
      private int doSomething() {
        Random random = new Random();
        int i = random.nextInt(3);
        System.out.println("i is " + i);
        return 10 / i;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    这里为了模拟异常的情况,阿粉在 doSomething 函数里面进行了随机数的生成和使用,当随机出来的值为 0 的时候,则会触发 java.lang.ArithmeticException 异常,因为 0 不能作除数。

    接下来我们再对外提供一个接口用于访问,代码如下

    package com.example.demo.controller;
    
    import com.example.demo.service.HelloService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class HelloController {
    
      @Autowired
      private HelloService helloService;
    
      @GetMapping(value = "/hello")
      public String hello(@RequestParam("name") String name) {
        return helloService.sayHello(name);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    正常启动过后,我们通过浏览器进行访问。

    可以看到,我们第一次方法的时候就成功的达到了我们要的效果,随机数就是 0 ,在 1 秒后重试后结果正常。在多试了几次过后,会遇到三次都是 0 的情况,这个时候才会抛出异常,说明服务是真的有问题了。

    上面的代码可以看到是有效果了,虽然不是很好看,特别是在还有一些其他逻辑的情况,看上去会很臃肿,但是确实是可以正常使用的,那么有的小伙伴就要问了,有没有一种优雅的方式呢?总不能在很多地方都重复的这样写重试的代码吧。

    注解重试

    要知道我们普通人在日常开发的时候,如果遇到一个问题肯定是别人都遇到过的,什么时候当我们遇到的问题,没有人遇到过的时候,那说明我们是很前卫的。

    因此小伙伴能想到的是不是有简单的方式来进行重试,有的人已经帮我们想好了,可以通过 @Retryable 注解来实现一样的效果,接下来阿粉就给大家演示一下如何使用这个注解。

    首先我们需要在启动类上面加入 @EnableRetry 注解,表示要开启重试的功能,这个很好理解,就像我们要开启定时功能需要添加 @EnableScheduling 注解一样,Spring 的 @Enablexxx 注解也是很有意思的,后面我们再聊。

    添加完注解以后,需要加入切面的依赖,如下

    
      org.aspectj
      aspectjweaver
      1.9.2
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    如下不加入这个切面依赖,启动的时候会有如下异常

    添加的注解和依赖过后,我们需要改造 HelloService 里面的 sayHello() 方法,简化成如下,增加  @Retryable 注解,以及设置相应的参数值。

    @Retryable(value = Exception.class, maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 2))
      public String sayHello(String name){
        return name + doSomething();
      }
    
    • 1
    • 2
    • 3
    • 4

    再次通过浏览器访问 http://127.0.0.1:8080/hello?name=ziyou 我们看到效果如下,跟我们自己写的重试一样。

    @Retryable 详解
    //
    // Source code recreated from a .class file by IntelliJ IDEA
    // (powered by FernFlower decompiler)
    //
    
    package org.springframework.retry.annotation;
    
    import java.lang.annotation.Documented;
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Target({ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface Retryable {
        String recover() default "";
    
        String interceptor() default "";
    
        Class[] value() default {};
    
        Class[] include() default {};
    
        Class[] exclude() default {};
    
        String label() default "";
    
        boolean stateful() default false;
    
        int maxAttempts() default 3;
    
        String maxAttemptsExpression() default "";
    
        Backoff backoff() default @Backoff;
    
        String exceptionExpression() default "";
    
        String[] listeners() default {};
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    点到这个注解里面,我们可以看到这个注解的代码如下,其中有几个参数我们来解释一下

    • recover:  当前类中的回滚方法名称;
    • interceptor: 重试的拦截器名称,重试的时候可以配置一个拦截器;
    • value:需要重试的异常类型,跟下面的 include 一致;
    • include:包含的重试的异常类型;
    • exclude:不包含的重试异常类型;
    • label:用于统计的唯一标识;
    • stateful:标志表示重试是有状态的,也就是说,异常被重新抛出,重试策略是否会以相同的策略应用于具有相同参数的后续调用。如果是false,那么可重试的异常就不会被重新抛出。
    • maxAttempts:重试次数;
    • backoff:指定用于重试此操作的属性;
    • listeners:重试监听器bean 名称;

    配合上面的一些属性的使用,我们就可以达到通过注解简单来实现方法调用异常后的自动重试,非常好用。我们可以在执行重试方法的时候设置自定义的重试拦截器,如下所示,自定义重试拦截器需要实现 MethodInterceptor 接口并实现 invoke 方法,不过要注意,如果使用了拦截器的话,那么方法上的参数就会被覆盖。

    package com.example.demo.pid;
    
    import org.aopalliance.intercept.MethodInterceptor;
    import org.aopalliance.intercept.MethodInvocation;
    import org.springframework.retry.interceptor.RetryInterceptorBuilder;
    import org.springframework.retry.interceptor.RetryOperationsInterceptor;
    import org.springframework.retry.policy.SimpleRetryPolicy;
    import org.springframework.stereotype.Component;
    
    @Component
    public class CustomRetryInterceptor implements MethodInterceptor {
    
      @Override
      public Object invoke(MethodInvocation invocation) throws Throwable {
        RetryOperationsInterceptor build = RetryInterceptorBuilder.stateless()
          .maxAttempts(2).backOffOptions(3000, 2, 1000).build();
        return build.invoke(invocation);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    自定义回滚方法,我们还可以在重试几次依旧错误的情况,编写自定义的回滚方法。

    @Retryable(value = Exception.class,
        recover = "recover", maxAttempts = 2,
        backoff = @Backoff(delay = 1000, multiplier = 2))
      public String sayHello(String name){
        return name + doSomething();
      }
    
      @Recover
      public String recover(Exception e, String name) {
        System.out.println("recover");
        return "recover";
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    要注意:

    • 重试方法必须要使用@Recover 注解;
    • 返回值必须和被重试的函数返回值一致;
    • 参数中除了第一个是触发的异常外,后面的参数需要和被重试函数的参数列表一致;

    上面代码中的 @Backoff(delay = 1000, multiplier = 2) 表示第一次延迟 1000ms 重试,后面每次重试的延迟时间都翻倍。

    总结
  • 相关阅读:
    Java入门 实用类 (二)(第二十四天)
    Java8特性-Lambda表达式
    LeetCode_21_简单_合并两个有序链表
    archlinux解决fcitx5光标不跟随
    保研经历分享(一)
    【爬虫】基于matlab实现火车票信息爬虫
    Integrated LogicAnalyzer v6.2 (Vivado ILA使用方法)
    hdu3549Flow Problem(最大流模板题)
    js数组介绍:创建、length的用法、冒泡排序法、选择排序法
    MATLAB BP神经网络 笔记整理
  • 原文地址:https://blog.csdn.net/Huangjiazhen711/article/details/126866937