文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Java中重试机制的方式有哪些

2023-07-05 12:39

关注

今天小编给大家分享一下Java中重试机制的方式有哪些的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。

重试机制在分布式系统中,或者调用外部接口中,都是十分重要的。

重试机制可以保护系统减少因网络波动、依赖服务短暂性不可用带来的影响,让系统能更稳定的运行的一种保护机制。

为了方便说明,先假设我们想要进行重试的方法如下:

@Slf4j@Componentpublic class HelloService {     private static AtomicLong helloTimes = new AtomicLong();     public String hello(){        long times = helloTimes.incrementAndGet();        if (times % 4 != 0){            log.warn("发生异常,time:{}", LocalTime.now() );            throw new HelloRetryException("发生Hello异常");        }        return "hello";    }}

调用处:

@Slf4j@Servicepublic class HelloRetryService implements IHelloService{     @Autowired    private HelloService helloService;     public String hello(){        return helloService.hello();    }}

也就是说,这个接口每调4次才会成功一次。

1.手动重试

先来用最简单的方法,直接在调用的时候进重试:

// 手动重试public String hello(){    int maxRetryTimes = 4;    String s = "";    for (int retry = 1; retry <= maxRetryTimes; retry++) {        try {            s = helloService.hello();            log.info("helloService返回:{}", s);            return s;        } catch (HelloRetryException e) {            log.info("helloService.hello() 调用失败,准备重试");        }    }    throw new HelloRetryException("重试次数耗尽");}

发生异常,time:10:17:21.079413300
helloService.hello() 调用失败,准备重试
发生异常,time:10:17:21.085861800
helloService.hello() 调用失败,准备重试
发生异常,time:10:17:21.085861800
helloService.hello() 调用失败,准备重试
helloService返回:hello
service.helloRetry():hello

程序在极短的时间内进行了4次重试,然后成功返回。

这样虽然看起来可以解决问题,但实践上,由于没有重试间隔,很可能当时依赖的服务尚未从网络异常中恢复过来,所以极有可能接下来的几次调用都是失败的。

而且,这样需要对代码进行大量的侵入式修改,显然,不优雅。

2.代理模式

上面的处理方式由于需要对业务代码进行大量修改,虽然实现了功能,但是对原有代码的侵入性太强,可维护性差。

所以需要使用一种更优雅一点的方式,不直接修改业务代码,那要怎么做呢?

其实很简单,直接在业务代码的外面再包一层就行了,代理模式在这里就有用武之地了。你会发现又是代理。

@Slf4jpublic class HelloRetryProxyService implements IHelloService{       @Autowired    private HelloRetryService helloRetryService;        @Override    public String hello() {        int maxRetryTimes = 4;        String s = "";        for (int retry = 1; retry <= maxRetryTimes; retry++) {            try {                s = helloRetryService.hello();                log.info("helloRetryService 返回:{}", s);                return s;            } catch (HelloRetryException e) {                log.info("helloRetryService.hello() 调用失败,准备重试");            }        }        throw new HelloRetryException("重试次数耗尽");    }}

这样,重试逻辑就都由代理类来完成,原业务类的逻辑就不需要修改了,以后想修改重试逻辑也只需要修改这个类就行了,分工明确。比如,现在想要在重试之间加上一个延迟,只需要做一点点修改即可:

@Overridepublic String hello() {    int maxRetryTimes = 4;    String s = "";    for (int retry = 1; retry <= maxRetryTimes; retry++) {        try {            s = helloRetryService.hello();            log.info("helloRetryService 返回:{}", s);            return s;        } catch (HelloRetryException e) {            log.info("helloRetryService.hello() 调用失败,准备重试");        }        // 延时一秒        try {            Thread.sleep(1000);        } catch (InterruptedException e) {            e.printStackTrace();        }    }    throw new HelloRetryException("重试次数耗尽");}

代理模式虽然要更加优雅,但是如果依赖的服务很多的时候,要为每个服务都创建一个代理类,显然过于麻烦,而且其实重试的逻辑都大同小异,无非就是重试的次数和延时不一样而已。

如果每个类都写这么一长串类似的代码,显然,不优雅!

3.JDK动态代理

这时候,动态代理就闪亮登场了。只需要写一个代理处理类就ok了。

@Slf4jpublic class RetryInvocationHandler implements InvocationHandler {     private final Object subject;     public RetryInvocationHandler(Object subject) {        this.subject = subject;    }     @Override    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {        int times = 0;         while (times < RetryConstant.MAX_TIMES) {            try {                return method.invoke(subject, args);            } catch (Exception e) {                times++;                log.info("times:{},time:{}", times, LocalTime.now());                if (times >= RetryConstant.MAX_TIMES) {                    throw new RuntimeException(e);                }            }             // 延时一秒            try {                Thread.sleep(1000);            } catch (InterruptedException e) {                e.printStackTrace();            }        }         return null;    }         public static Object getProxy(Object realSubject) {        InvocationHandler handler = new RetryInvocationHandler(realSubject);        return Proxy.newProxyInstance(handler.getClass().getClassLoader(),                realSubject.getClass().getInterfaces(), handler);    } }

咱们测试一下:

@Testpublic void helloDynamicProxy() {    IHelloService realService = new HelloService();    IHelloService proxyService = (IHelloService)RetryInvocationHandler.getProxy(realService);     String hello = proxyService.hello();    log.info("hello:{}", hello);}

输出结果如下:

hello times:1
发生异常,time:11:22:20.727586700
times:1,time:11:22:20.728083
hello times:2
发生异常,time:11:22:21.728858700
times:2,time:11:22:21.729343700
hello times:3
发生异常,time:11:22:22.729706600
times:3,time:11:22:22.729706600
hello times:4
hello:hello

在重试了4次之后输出了Hello,符合预期。

动态代理可以将重试逻辑都放到一块,显然比直接使用代理类要方便很多,也更加优雅。

不过不要高兴的太早,这里因为被代理的HelloService是一个简单的类,没有依赖其它类,所以直接创建是没有问题的,但如果被代理的类依赖了其它被Spring容器管理的类,则这种方式会抛出异常,因为没有把被依赖的实例注入到创建的代理实例中。

这种情况下,就比较复杂了,需要从Spring容器中获取已经装配好的,需要被代理的实例,然后为其创建代理类实例,并交给Spring容器来管理,这样就不用每次都重新创建新的代理类实例了。

话不多说,撸起袖子就是干。

新建一个工具类,用来获取代理实例:

@Componentpublic class RetryProxyHandler {     @Autowired    private ConfigurableApplicationContext context;     public Object getProxy(Class clazz) {        // 1. 从Bean中获取对象        DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory)context.getAutowireCapableBeanFactory();        Map<String, Object> beans = beanFactory.getBeansOfType(clazz);        Set<Map.Entry<String, Object>> entries = beans.entrySet();        if (entries.size() <= 0){            throw new ProxyBeanNotFoundException();        }        // 如果有多个候选bean, 判断其中是否有代理bean        Object bean = null;        if (entries.size() > 1){            for (Map.Entry<String, Object> entry : entries) {                if (entry.getKey().contains(PROXY_BEAN_SUFFIX)){                    bean = entry.getValue();                }            };            if (bean != null){                return bean;            }            throw new ProxyBeanNotSingleException();        }         Object source = beans.entrySet().iterator().next().getValue();        Object source = beans.entrySet().iterator().next().getValue();         // 2. 判断该对象的代理对象是否存在        String proxyBeanName = clazz.getSimpleName() + PROXY_BEAN_SUFFIX;        Boolean exist = beanFactory.containsBean(proxyBeanName);        if (exist) {            bean = beanFactory.getBean(proxyBeanName);            return bean;        }         // 3. 不存在则生成代理对象        bean = RetryInvocationHandler.getProxy(source);         // 4. 将bean注入spring容器        beanFactory.registerSingleton(proxyBeanName, bean);        return bean;    }}

使用的是JDK动态代理:

@Slf4jpublic class RetryInvocationHandler implements InvocationHandler {     private final Object subject;     public RetryInvocationHandler(Object subject) {        this.subject = subject;    }     @Override    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {        int times = 0;         while (times < RetryConstant.MAX_TIMES) {            try {                return method.invoke(subject, args);            } catch (Exception e) {                times++;                log.info("retry times:{},time:{}", times, LocalTime.now());                if (times >= RetryConstant.MAX_TIMES) {                    throw new RuntimeException(e);                }            }             // 延时一秒            try {                Thread.sleep(1000);            } catch (InterruptedException e) {                e.printStackTrace();            }        }         return null;    }         public static Object getProxy(Object realSubject) {        InvocationHandler handler = new RetryInvocationHandler(realSubject);        return Proxy.newProxyInstance(handler.getClass().getClassLoader(),                realSubject.getClass().getInterfaces(), handler);    } }

至此,主要代码就完成了,修改一下HelloService类,增加一个依赖:

@Slf4j@Componentpublic class HelloService implements IHelloService{     private static AtomicLong helloTimes = new AtomicLong();     @Autowired    private NameService nameService;     public String hello(){        long times = helloTimes.incrementAndGet();        log.info("hello times:{}", times);        if (times % 4 != 0){            log.warn("发生异常,time:{}", LocalTime.now() );            throw new HelloRetryException("发生Hello异常");        }        return "hello " + nameService.getName();    }}

NameService其实很简单,创建的目的仅在于测试依赖注入的Bean能否正常运行。

@Servicepublic class NameService {     public String getName(){        return "Frank";    }}

测试一下:

@Testpublic void helloJdkProxy() throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {    IHelloService proxy = (IHelloService) retryProxyHandler.getProxy(HelloService.class);    String hello = proxy.hello();    log.info("hello:{}", hello);}

结果:

hello times:1
发生异常,time:14:40:27.540672200
retry times:1,time:14:40:27.541167400
hello times:2
发生异常,time:14:40:28.541584600
retry times:2,time:14:40:28.542033500
hello times:3
发生异常,time:14:40:29.542161500
retry times:3,time:14:40:29.542161500
hello times:4
hello:hello Frank

完美,这样就不用担心依赖注入的问题了,因为从Spring容器中拿到的Bean对象都是已经注入配置好的。当然,这里仅考虑了单例Bean的情况,可以考虑的更加完善一点,判断一下容器中Bean的类型是Singleton还是Prototype,如果是Singleton则像上面这样进行操作,如果是Prototype则每次都新建代理类对象。

另外,这里使用的是JDK动态代理,因此就存在一个天然的缺陷,如果想要被代理的类,没有实现任何接口,那么就无法为其创建代理对象,这种方式就行不通了。

4.Spring AOP

想要无侵入式的修改原有逻辑?想要一个注解就实现重试?用Spring AOP不就能完美实现吗?使用AOP来为目标调用设置切面,即可在目标方法调用前后添加一些额外的逻辑。

先创建一个注解:

@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface Retryable {    int retryTimes() default 3;    int retryInterval() default 1;}

有两个参数,retryTimes 代表最大重试次数,retryInterval代表重试间隔。

@Retryable(retryTimes = 4, retryInterval = 2)public String hello(){    long times = helloTimes.incrementAndGet();    log.info("hello times:{}", times);    if (times % 4 != 0){        log.warn("发生异常,time:{}", LocalTime.now() );        throw new HelloRetryException("发生Hello异常");    }    return "hello " + nameService.getName();}

接着,进行最后一步,编写AOP切面:

@Slf4j@Aspect@Componentpublic class RetryAspect {     @Pointcut("@annotation(com.mfrank.springboot.retry.demo.annotation.Retryable)")    private void retryMethodCall(){}     @Around("retryMethodCall()")    public Object retry(ProceedingJoinPoint joinPoint) throws InterruptedException {        // 获取重试次数和重试间隔        Retryable retry = ((MethodSignature)joinPoint.getSignature()).getMethod().getAnnotation(Retryable.class);        int maxRetryTimes = retry.retryTimes();        int retryInterval = retry.retryInterval();         Throwable error = new RuntimeException();        for (int retryTimes = 1; retryTimes <= maxRetryTimes; retryTimes++){            try {                Object result = joinPoint.proceed();                return result;            } catch (Throwable throwable) {                error = throwable;                log.warn("调用发生异常,开始重试,retryTimes:{}", retryTimes);            }            Thread.sleep(retryInterval * 1000);        }        throw new RetryExhaustedException("重试次数耗尽", error);    }}

开始测试:

@Autowiredprivate HelloService helloService; @Testpublic void helloAOP(){    String hello = helloService.hello();    log.info("hello:{}", hello);}

打印结果:

hello times:1
发生异常,time:16:49:30.224649800
调用发生异常,开始重试,retryTimes:1
hello times:2
发生异常,time:16:49:32.225230800
调用发生异常,开始重试,retryTimes:2
hello times:3
发生异常,time:16:49:34.225968900
调用发生异常,开始重试,retryTimes:3
hello times:4
hello:hello Frank

这样就相当优雅了,一个注解就能搞定重试,简直不要更棒。

5.Spring 的重试注解

实际上Spring中就有比较完善的重试机制,比上面的切面更加好用,还不需要自己动手重新造轮子。

那让我们先来看看这个轮子究竟好不好使。

先引入重试所需的jar包:

<dependency>    <groupId>org.springframework.retry</groupId>    <artifactId>spring-retry</artifactId></dependency><dependency>    <groupId>org.aspectj</groupId>    <artifactId>aspectjweaver</artifactId></dependency>

然后在启动类或者配置类上添加@EnableRetry注解,接下来在需要重试的方法上添加@Retryable注解(嗯?好像跟我自定义的注解一样?竟然抄袭我的注解【手动狗头】)

@Retryablepublic String hello(){    long times = helloTimes.incrementAndGet();    log.info("hello times:{}", times);    if (times % 4 != 0){        log.warn("发生异常,time:{}", LocalTime.now() );        throw new HelloRetryException("发生Hello异常");    }    return "hello " + nameService.getName();}

默认情况下,会重试三次,重试间隔为1秒。当然我们也可以自定义重试次数和间隔。这样就跟我前面实现的功能是一毛一样的了。

但Spring里的重试机制还支持很多很有用的特性,比如说,可以指定只对特定类型的异常进行重试,这样如果抛出的是其它类型的异常则不会进行重试,就可以对重试进行更细粒度的控制。默认为空,会对所有异常都重试。

@Retryable{value = {HelloRetryException.class}}public String hello(){    ...}

也可以使用include和exclude来指定包含或者排除哪些异常进行重试。

可以用maxAttemps指定最大重试次数,默认为3次。

可以用interceptor设置重试拦截器的bean名称。

可以通过label设置该重试的唯一标志,用于统计输出。

可以使用exceptionExpression来添加异常表达式,在抛出异常后执行,以判断后续是否进行重试。

此外,Spring中的重试机制还支持使用backoff来设置重试补偿机制,可以设置重试间隔,并且支持设置重试延迟倍数。

举个例子:

@Retryable(value = {HelloRetryException.class}, maxAttempts = 5,           backoff = @Backoff(delay = 1000, multiplier = 2))public String hello(){    ...}

该方法调用将会在抛出HelloRetryException异常后进行重试,最大重试次数为5,第一次重试间隔为1s,之后以2倍大小进行递增,第二次重试间隔为2s,第三次为4s,第四次为8s。

重试机制还支持使用@Recover 注解来进行善后工作,当重试达到指定次数之后,将会调用该方法,可以在该方法中进行日志记录等操作。

这里值得注意的是,想要@Recover 注解生效的话,需要跟被@Retryable 标记的方法在同一个类中,且被@Retryable 标记的方法不能有返回值,否则不会生效。

并且如果使用了@Recover注解的话,重试次数达到最大次数后,如果在@Recover标记的方法中无异常抛出,是不会抛出原异常的。

@Recoverpublic boolean recover(Exception e) {    log.error("达到最大重试次数",e);    return false;}

除了使用注解外,Spring Retry 也支持直接在调用时使用代码进行重试:

@Testpublic void normalSpringRetry() {    // 表示哪些异常需要重试,key表示异常的字节码,value为true表示需要重试    Map<Class<? extends Throwable>, Boolean> exceptionMap = new HashMap<>();    exceptionMap.put(HelloRetryException.class, true);     // 构建重试模板实例    RetryTemplate retryTemplate = new RetryTemplate();     // 设置重试回退操作策略,主要设置重试间隔时间    FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();    long fixedPeriodTime = 1000L;    backOffPolicy.setBackOffPeriod(fixedPeriodTime);     // 设置重试策略,主要设置重试次数    int maxRetryTimes = 3;    SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(maxRetryTimes, exceptionMap);     retryTemplate.setRetryPolicy(retryPolicy);    retryTemplate.setBackOffPolicy(backOffPolicy);     Boolean execute = retryTemplate.execute(        //RetryCallback        retryContext -> {            String hello = helloService.hello();            log.info("调用的结果:{}", hello);            return true;        },        // RecoverCallBack        retryContext -> {            //RecoveryCallback            log.info("已达到最大重试次数");            return false;        }    );}

此时唯一的好处是可以设置多种重试策略:

悲观组合重试策略是指只要有一个策略不允许即可以重试,但不管哪种组合方式,组合中的每一个策略都会执行

可以看出,Spring中的重试机制还是相当完善的,比上面自己写的AOP切面功能更加强大。

这里还需要再提醒的一点是,由于Spring Retry用到了Aspect增强,所以就会有使用Aspect不可避免的坑&mdash;&mdash;方法内部调用,如果被 @Retryable 注解的方法的调用方和被调用方处于同一个类中,那么重试将会失效。

但也还是存在一定的不足,Spring的重试机制只支持对异常进行捕获,而无法对返回值进行校验。

以上就是“Java中重试机制的方式有哪些”这篇文章的所有内容,感谢各位的阅读!相信大家阅读完这篇文章都有很大的收获,小编每天都会为大家更新不同的知识,如果还想学习更多的知识,请关注编程网行业资讯频道。

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯