我的编程空间,编程开发者的网络收藏夹
学习永远不晚

【重试】Java 中的 7 种重试机制

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

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

【重试】Java 中的 7 种重试机制

随着互联网的发展项目中的业务功能越来越复杂,有一些基础服务我们不可避免的会去调用一些第三方的接口或者公司内其他项目中提供的服务,但是远程服务的健壮性和网络稳定性都是不可控因素。在测试阶段可能没有什么异常情况,但上线后可能会出现调用的接口因为内部错误或者网络波动而出错或返回系统异常,因此我们必须考虑加上重试机制

重试机制 可以提高系统的健壮性,并且减少因网络波动依赖服务临时不可用带来的影响,让系统能更稳定的运行

1. 手动重试

手动重试:使用 while 语句进行重试:

@Servicepublic class OrderServiceImpl implements OrderService {public void addOrder() {    int times = 1;    while (times <= 5) {        try {            // 故意抛异常            int i = 3 / 0;            // addOrder        } catch (Exception e) {            System.out.println("重试" + times + "次");            Thread.sleep(2000);            times++;            if (times > 5) {                throw new RuntimeException("不再重试!");            }        }    }}}

运行上述代码:

在这里插入图片描述

上述代码看上去可以解决重试问题,但实际上存在一些弊端:

  1. 由于没有重试间隔,很可能远程调用的服务还没有从网络异常中恢复,所以有可能接下来的几次调用都会失败
  2. 代码侵入式太高,调用方代码不够优雅
  3. 项目中远程调用的服务可能有很多,每个都去添加重试会出现大量的重复代码

2. 静态代理

上面的处理方式由于需要对业务代码进行大量修改,虽然实现了功能,但是对原有代码的侵入性太强,可维护性差。所以需要使用一种更优雅一点的方式,不直接修改业务代码,那要怎么做呢?

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

@Servicepublic class OrderServiceProxyImpl implements OrderService {        @Autowired    private OrderServiceImpl orderService;    @Override    public void addOrder() {        int times = 1;        while (times <= 5) {            try {                // 故意抛异常                int i = 3 / 0;                orderService.addOrder();            } catch (Exception e) {                System.out.println("重试" + times + "次");                try {                    Thread.sleep(2000);                } catch (InterruptedException ex) {                    ex.printStackTrace();                }                times++;                if (times > 5) {                    throw new RuntimeException("不再重试!");                }            }        }            }}

这样,重试逻辑就都由代理类来完成,原业务类的逻辑就不需要修改了,以后想修改重试逻辑也只需要修改这个类就行了

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

3. JDK 动态代理

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

public 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 = 1;        while (times <= 5) {            try {                // 故意抛异常                int i = 3 / 0;                return method.invoke(subject, args);            } catch (Exception e) {                System.out.println("重试【" + times + "】次");                try {                    Thread.sleep(2000);                } catch (InterruptedException ex) {                    ex.printStackTrace();                }                times++;                if (times > 5) {                    throw new RuntimeException("不再重试!");                }            }        }        return null;    }    public static Object getProxy(Object realSubject) {        InvocationHandler handler = new RetryInvocationHandler(realSubject);        return Proxy.newProxyInstance(handler.getClass().getClassLoader(), realSubject.getClass().getInterfaces(), handler);    }}

测试:

@RestController@RequestMapping("/order")public class OrderController {    @Qualifier("orderServiceImpl")    @Autowired    private OrderService orderService;    @GetMapping("/addOrder")    public String addOrder() {        OrderService orderServiceProxy = (OrderService)RetryInvocationHandler.getProxy(orderService);        orderServiceProxy.addOrder();        return "addOrder";    }    }

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

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

4. CGLib 动态代理

既然已经说到了 JDK 动态代理,那就不得不提 CGLib 动态代理了。使用 JDK 动态代理对被代理的类有要求,不是所有的类都能被代理,而 CGLib 动态代理则刚好解决了这个问题

@Componentpublic class CGLibRetryProxyHandler implements MethodInterceptor {    private Object target;    @Override    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {        int times = 1;        while (times <= 5) {            try {                // 故意抛异常                int i = 3 / 0;                return method.invoke(target, objects);            } catch (Exception e) {                System.out.println("重试【" + times + "】次");                try {                    Thread.sleep(2000);                } catch (InterruptedException ex) {                    ex.printStackTrace();                }                times++;                if (times > 5) {                    throw new RuntimeException("不再重试!");                }            }        }        return null;    }    public Object getCglibProxy(Object objectTarget){        this.target = objectTarget;        Enhancer enhancer = new Enhancer();        enhancer.setSuperclass(objectTarget.getClass());        enhancer.setCallback(this);        Object result = enhancer.create();        return result;    }}

测试:

@GetMapping("/addOrder")public String addOrder() {    OrderService orderServiceProxy = (OrderService) cgLibRetryProxyHandler.getCglibProxy(orderService);    orderServiceProxy.addOrder();    return "addOrder";}

这样就很棒了,完美的解决了 JDK 动态代理带来的缺陷。优雅指数上涨了不少。

但这个方案仍旧存在一个问题,那就是需要对原来的逻辑进行侵入式修改,在每个被代理实例被调用的地方都需要进行调整,这样仍然会对原有代码带来较多修改

5. 手动 Aop

考虑到以后可能会有很多的方法也需要重试功能,咱们可以将重试这个共性功能通过 AOP 来实现:使用 AOP 来为目标调用设置切面,即可在目标方法调用前后添加一些重试的逻辑

<dependency>    <groupId>org.aspectjgroupId>    <artifactId>aspectjweaverartifactId>dependency>

自定义注解:

@Documented@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface MyRetryable {        // 最大重试次数    int retryTimes() default 3;    // 重试间隔    int retryInterval() default 1;}
@Slf4j@Aspect@Componentpublic class RetryAspect {    @Pointcut("@annotation(com.hcr.sbes.retry.annotation.MyRetryable)")    private void retryMethodCall(){}    @Around("retryMethodCall()")    public Object retry(ProceedingJoinPoint joinPoint) throws InterruptedException {        // 获取重试次数和重试间隔        MyRetryable retry = ((MethodSignature)joinPoint.getSignature()).getMethod().getAnnotation(MyRetryable.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 * 1000L);        }        throw new RuntimeException("重试次数耗尽", error);    }}

给需要重试的方法添加注解 @MyRetryable

@Servicepublic class OrderServiceImpl implements OrderService {    @Override    @MyRetryable(retryTimes = 5, retryInterval = 2)    public void addOrder() {        int i = 3 / 0;        // addOrder    }    }

这样即不用编写重复代码,实现上也比较优雅了:一个注解就实现重试。

6. spring-retry

<dependency>    <groupId>org.springframework.retrygroupId>    <artifactId>spring-retryartifactId>dependency>

开启重试功能:在启动类或者配置类上添加 @EnableRetry 注解

在需要重试的方法上添加 @Retryable 注解

@Slf4j@Servicepublic class OrderServiceImpl implements OrderService {    @Override    @Retryable(maxAttempts = 3, backoff = @Backoff(delay = 2000, multiplier = 2))    public void addOrder() {        System.out.println("重试...");        int i = 3 / 0;        // addOrder    }    @Recover    public void recover(RuntimeException e) {        log.error("达到最大重试次数", e);    }    }

该方法调用后会进行重试,最大重试次数为 3,第一次重试间隔为 2s,之后以 2 倍大小进行递增,第二次重试间隔为 4 s,第三次为 8s

Spring 的重试机制还支持很多很有用的特性,由三个注解完成:

  • @Retryable
  • @Backoff
  • @Recover

查看 @Retryable 注解源码:指定异常重试、次数

public @interface Retryable {// 设置重试拦截器的 bean 名称    String interceptor() default "";// 只对特定类型的异常进行重试。默认:所有异常    Class<? extends Throwable>[] value() default {};// 包含或者排除哪些异常进行重试    Class<? extends Throwable>[] include() default {};    Class<? extends Throwable>[] exclude() default {};// l设置该重试的唯一标志,用于统计输出    String label() default "";    boolean stateful() default false;// 最大重试次数,默认为 3 次    int maxAttempts() default 3;    String maxAttemptsExpression() default "";// 设置重试补偿机制,可以设置重试间隔,并且支持设置重试延迟倍数    Backoff backoff() default @Backoff;// 异常表达式,在抛出异常后执行,以判断后续是否进行重试    String exceptionExpression() default "";    String[] listeners() default {};}

@Backoff 注解:指定重试回退策略(如果因为网络波动导致调用失败,立即重试可能还是会失败,最优选择是等待一小会儿再重试。决定等待多久之后再重试的方法。通俗的说,就是每次重试是立即重试还是等待一段时间后重试)

@Recover 注解:进行善后工作:当重试达到指定次数之后,会调用指定的方法来进行日志记录等操作

注意:

  1. @Recover 注解标记的方法必须和被 @Retryable 标记的方法在同一个类中
  2. 重试方法抛出的异常类型需要与 recover() 方法参数类型保持一致
  3. recover() 方法返回值需要与重试方法返回值保证一致
  4. recover() 方法中不能再抛出 Exception,否则会报无法识别该异常的错误

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

通过以上几个简单的配置,可以看到 Spring Retry 重试机制考虑的比较完善,比自己写AOP实现要强大很多

弊端:

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

@Retryablepublic String hello() {    long current = count.incrementAndGet();    System.out.println("第" + current +"次被调用");    if (current % 3 != 0) {        log.warn("调用失败");        return "error";    }    return "success";}

因此就算在方法上添加 @Retryable,也无法实现失败重试

除了使用注解外,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;        }    );}

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

  1. NeverRetryPolicy:只允许调用RetryCallback一次,不允许重试
  2. AlwaysRetryPolicy:允许无限重试,直到成功,此方式逻辑不当会导致死循环
  3. SimpleRetryPolicy:固定次数重试策略,默认重试最大次数为3次,RetryTemplate默认使用的策略
  4. TimeoutRetryPolicy:超时时间重试策略,默认超时时间为1秒,在指定的超时时间内允许重试
  5. ExceptionClassifierRetryPolicy:设置不同异常的重试策略,类似组合重试策略,区别在于这里只区分不同异常的重试
  6. CircuitBreakerRetryPolicy:有熔断功能的重试策略,需设置3个参数openTimeout、resetTimeout和delegate
  7. CompositeRetryPolicy:组合重试策略,有两种组合方式,乐观组合重试策略是指只要有一个策略允许即可以重试,
    悲观组合重试策略是指只要有一个策略不允许即可以重试,但不管哪种组合方式,组合中的每一个策略都会执行

7. guava-retry

和 Spring Retry 相比,Guava Retry 具有更强的灵活性,并且能够根据 返回值 来判断是否需要重试

<dependency>    <groupId>com.github.rholdergroupId>    <artifactId>guava-retryingartifactId>    <version>2.0.0version>dependency>
@Overridepublic String guavaRetry(Integer num) {    Retryer<String> retryer = RetryerBuilder.<String>newBuilder()            //无论出现什么异常,都进行重试            .retryIfException()            //返回结果为 error时,进行重试            .retryIfResult(result -> Objects.equals(result, "error"))            //重试等待策略:等待 2s 后再进行重试            .withWaitStrategy(WaitStrategies.fixedWait(2, TimeUnit.SECONDS))            //重试停止策略:重试达到 3 次            .withStopStrategy(StopStrategies.stopAfterAttempt(3))            .withRetryListener(new RetryListener() {                @Override                public <V> void onRetry(Attempt<V> attempt) {                    System.out.println("RetryListener: 第" + attempt.getAttemptNumber() + "次调用");                }            })            .build();    try {        retryer.call(() -> testGuavaRetry(num));    } catch (Exception e) {        e.printStackTrace();    }    return "test";}

先创建一个Retryer实例,然后使用这个实例对需要重试的方法进行调用,可以通过很多方法来设置重试机制:

  • retryIfException():对所有异常进行重试
  • retryIfRuntimeException():设置对指定异常进行重试
  • retryIfExceptionOfType():对所有 RuntimeException 进行重试
  • retryIfResult():对不符合预期的返回结果进行重试

还有五个以 withXxx 开头的方法,用来对重试策略/等待策略/阻塞策略/单次任务执行时间限制/自定义监听器进行设置,以实现更加强大的异常处理:

  • withRetryListener():设置重试监听器,用来执行额外的处理工作
  • withWaitStrategy():重试等待策略
  • withStopStrategy():停止重试策略
  • withAttemptTimeLimiter:设置任务单次执行的时间限制,如果超时则抛出异常
  • withBlockStrategy():设置任务阻塞策略,即可以设置当前重试完成,下次重试开始前的这段时间做什么事情

8. 总结

从手动重试,到使用 Spring AOP 自己动手实现,再到站在巨人肩上使用特别优秀的开源实现 Spring Retry 和 Google guava-retrying,经过对各种重试实现方式的介绍,可以看到以上几种方式基本上已经满足大部分场景的需要:

  1. 如果是基于 Spring 的项目,使用 Spring Retry 的注解方式已经可以解决大部分问题
  2. 如果项目没有使用 Spring 相关框架,则适合使用 Google guava-retrying:自成体系,使用起来更加灵活强大

【转载】Java重试机制

Java教程:重试实现高可用一览

来源地址:https://blog.csdn.net/sco5282/article/details/131390099

免责声明:

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

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

【重试】Java 中的 7 种重试机制

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

【重试】Java 中的 7 种重试机制

随着互联网的发展项目中的业务功能越来越复杂,有一些基础服务我们不可避免的会去调用一些第三方的接口或者公司内其他项目中提供的服务,但是远程服务的健壮性和网络稳定性都是不可控因素。在测试阶段可能没有什么异常情况,但上线后可能会出现调用的接口因为
2023-08-21

深入浅出Java中重试机制的多种方式

重试机制在分布式系统中,或者调用外部接口中,都是十分重要的。重试机制可以保护系统减少因网络波动、依赖服务短暂性不可用带来的影响,让系统能更稳定的运行的一种保护机制。本文就来和大家聊聊Java中重试机制的多种方式
2023-03-14

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

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

如何使用Spring中的重试机制

这篇文章主要介绍“如何使用Spring中的重试机制”,在日常操作中,相信很多人在如何使用Spring中的重试机制问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”如何使用Spring中的重试机制”的疑惑有所帮助!
2023-06-15

Spring Cloud重试机制与各组件重试的示例分析

这篇文章将为大家详细讲解有关Spring Cloud重试机制与各组件重试的示例分析,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。SpringCloud重试机制配置首先声明一点,这里的重试并不是报错以后的重
2023-05-30

什么是Python重试机制

这篇文章主要讲解了“什么是Python重试机制”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“什么是Python重试机制”吧!为了避免由于一些网络或等其他不可控因素,而引起的功能性问题。比如在
2023-06-16

Python中retrying重试机制的示例分析

这篇文章主要介绍了Python中retrying重试机制的示例分析,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。循环加判断最简单的重试方式就是在需要进行重试的代码片段上加一个
2023-06-21

kafka的重试机制和ack机制是什么

Kafka的重试机制是指在消息发送过程中,如果发送失败或者出现异常,Kafka会自动尝试重新发送消息。重试机制的目的是确保消息能够成功发送到目标主题。Kafka的重试机制包括两个方面:Producer端重试:当Producer发送消息时,
2023-10-26

java重试机制使用RPC要考虑什么

这篇文章主要介绍“java重试机制使用RPC要考虑什么”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“java重试机制使用RPC要考虑什么”文章能帮助大家解决问题。1 为什么重试如果简单对一个RPC交
2023-07-05

Spark中的任务重试机制是指什么

在Spark中,任务重试机制是指当某个任务由于某种原因(例如节点故障、资源不足、网络问题等)失败时,Spark会自动尝试重新执行该任务,以确保作业能够成功完成。Spark会根据配置中设置的重试次数和策略进行任务重试,如指数退避策略等。通过任
Spark中的任务重试机制是指什么
2024-03-05

Spring Cloud中Zuul重试机制的示例分析

小编给大家分享一下Spring Cloud中Zuul重试机制的示例分析,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!具体内容如下:开启Zuul功能通过源码了解Zu
2023-05-30

Golang技巧之重试机制详解

Golang重试机制详解重试机制在分布式系统中至关重要,提高应用程序鲁棒性和可用性。Golang提供内置重试功能,涉及重试策略(Fixed、Exponential、Jitter)、退避算法(Constant、Linear、Exponential)和错误处理等概念。通过创建Backoff和Doer实例,指定重试策略和退避算法,即可在Golang中实现重试,并选择合适的策略和算法,避免无限重试,跟踪错误,善用上下文和异步重试,提升应用程序的健壮性和可靠性。
Golang技巧之重试机制详解
2024-04-24

Golang函数重试机制实现代码

Go中函数重试机制的实现在Go中,可通过错误处理和延迟机制实现函数重试。该机制包含自定义错误类型、函数内错误处理和延迟设置。代码示例展示了处理重试错误并设置延迟时间的实现。其他考虑因素包括重试次数限制、重试规则、错误记录和并发控制。该机制可根据需求定制,以满足不同场景。
Golang函数重试机制实现代码
2024-04-23

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录