【SpringBoot】spring-retry(重试机制)

2022-09-05 10:08:36

retry: 英/ˌriːˈtraɪ//ˌriːˈtraɪ/

概述

github地址

官网地址

  • 在调用第三方接口或者使用Mq时,会出现网络抖动,连接超时等网络异常,所以需要重试。

    网络抖动:标识一个网络的稳定性。抖动越小,网络越稳定。

  • Spring Retry是从Spring Batch 2.2.0版本独立出来的一个功能,主要实现了重试和熔断

  • 在 Spring Retry需要指定触发重试的异常类型,并设置每次重试的间隔以及如果重试失败是继续重试还是熔断(停止重试)。

    对于重试是有场景限制的,不是什么场景都适合重试,比如参数校验不合法、写操作等(要考虑写是否幂等)都不适合重试。远程调用超时、网络突然中断可以重试

    • 在微服务治理框架中,通常都有自己的重试与超时配置,比如dubbo可以设置retries=1,timeout=500调用失败只重试1次,超过500ms调用仍未返回则调用失败。

一.简单实现重试

  1. 异常捕获
  2. 循环重试(包括:重试次数,重试间隔)
publicclassTestRetry{//最大重试次数privatestaticfinal Integer tryTimes=6;//重试间隔时间单位秒privatestaticfinal Integer intervalTime=1;publicstaticvoidmain(String[] args)throws InterruptedException{boolean flag= TestRetry.retryBuss();
        System.out.println("最终执行结果:"+(flag?"成功":"失败"));}publicstaticbooleanretryBuss()throws InterruptedException{
        Integer retryNum=1;boolean flag=false;while(retryNum<= tryTimes){try{
                flag=execute(retryNum);if(flag){
                    System.out.println("第"+ retryNum+"次执行成功!!!");break;}
                System.err.println("第"+ retryNum+"次执行失败...");
                retryNum++;}catch(Exception e){
                retryNum++;
                TimeUnit.SECONDS.sleep(intervalTime);continue;}}return flag;}/**
     * 具体业务
     * @param retryNum
     * @return
     */privatestaticbooleanexecute(int retryNum){
        Random random=newRandom();int a= random.nextInt(10);boolean flag=true;try{if(a!=6){
                flag=false;thrownewRuntimeException();}}catch(Exception e){//这里捕获异常只是为了能,返回flag的结果}return flag;}}

失败情况
在这里插入图片描述

成功情况
在这里插入图片描述

二.声明式使用Spring-Retry

2.1.如何使用

1.引入依赖

<dependency><groupId>org.springframework.retry</groupId><artifactId>spring-retry</artifactId></dependency><dependency><!--如果其他的依赖已经引入了,可以不加--><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId></dependency>

2.配置类

  • 要使用@EnabelRetry开启重试才行,写在配置类或者启动类上都是可以的
@Configuration@EnableRetrypublicclassRetryConfiguration{@Beanpublic PayServicepayService(){returnnewPayService();}}

3.实现类

  • 服务类一般写主要逻辑,在需要重试的方法上面使用@Retryable
//@Service@Slf4jpublicclassPayService{privatefinalint totalNum=100000;@Retryable(value={Exception.class}, maxAttempts=3, backoff=@Backoff(delay=2000, multiplier=1.5))publicintminGoodsNum(int num){
        log.info("减库存开始=>"+ LocalTime.now());
        log.info("库存=>"+ totalNum);if(num<=0){thrownewIllegalArgumentException("数量不对");}
        log.info("减库存执行结束=>"+ LocalTime.now());return totalNum- num;}/**
     * 使用@Recover注解,当重试次数达到设置的次数的时候,还是失败抛出异常,执行的回调函数。
     */@Recoverpublicintrecover(Exception e){
        log.warn("减库存失败!!!"+ LocalTime.now());//记日志到数据库return totalNum;}}

测试代码

@RunWith(SpringRunner.class)@SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment.RANDOM_PORT)publicclassRetryTest{@Autowiredprivate PayService payService;/**
     * 应该使用 org.junit.Test 才是正确的
     */@TestpublicvoidminGoodsNum(){
        payService.minGoodsNum(-1);}}

结果
在这里插入图片描述

@Retryable修饰的minGoodsNum()方法,如果调用期间报了异常,那么它将进行重试3次(默认三次),如果超过最大重试次数,则执行@Retryable修饰的recover()方法,

@Retryable注解有各种配置,用于包含和排除异常类型、限制重试次数和回退策略。

2.2.常用注解

@EnableRetry:启用重试,proxyTargetClass属性为true时(默认false),使用CGLIB代理

@Retryable:标记当前方法会使用重试机制

  • value:指定抛出那些异常才会触发重试(可以配置多个异常类型) 默认为空
  • include:就是value默认为空,当exclude也为空时,默认所有异常都可以触发重试
  • exclude:指定那些异常不触发重试(可以配置多个异常类型),默认为空
  • maxAttempts:最大重试次数,默认3次包括第一次调用)
  • backoff:重试等待策略默认使用@Backoff注解
    在这里插入图片描述

@Backoff:重试回退策略(立即重试还是等待一会再重试)

  • value: 重试的间隔时间默认为1000L,我们设置为2000L
  • delay:重试的间隔时间,就是value
  • maxDelay:重试次数之间的最大时间间隔,默认为0,如果小于delay的设置,则默认为30000L
  • multiplier:delay时间的间隔倍数,默认为0,表示固定暂停1秒后进行重试,如果把multiplier设置为1.5,则第一次重试为2秒,第二次为3秒,第三次为4.5秒。
    .
  1. 不设置参数时,默认使用FixedBackOffPolicy(固定时间等待策略),重试等待1000ms
  2. 只设置delay时,使用FixedBackOffPolicy,重试等待指定的毫秒数
  3. 当设置delay和maxDealy时,重试等待在这两个值之间均态分布
  4. 设置delay,maxDealy和multiplier时,使用ExponentialBackOffPolicy(倍数等待策略)
  5. 当设置multiplier不等于0时,同时也设置了random时,使用ExponentialRandomBackOffPolicy(随机倍数等待策略)

@Recover标记方法为@Retryable失败时的“兜底”处理方法

  • 传参与@Retryable的配置的value必须一样
  • @Recover标记方法必须要与@Retryable注解的方法“形参”保持一致第一入参为要重试的异常(一定要是@Retryable方法里抛出的异常或者异常父类)其他参数与@Retryable保持一致,返回值也要一样,否则无法执行!

@CircuitBreaker:用于标记方法,实现熔断模式

  • include 指定处理的异常类。默认为空
  • exclude指定不需要处理的异常。默认为空
  • vaue指定要重试的异常。默认为空
  • maxAttempts 最大重试次数。默认3次
  • openTimeout 配置熔断器打开的超时时间,默认5s,当超过openTimeout之后熔断器电路变成半打开状态(只要有一次重试成功,则闭合电路)
  • resetTimeout 配置熔断器重新闭合的超时时间,默认20s,超过这个时间断路器关闭

2.3.注意事项

  • 使用了@Retryable注解的方法直接实例化调用不会触发重试,要先将实现类实例化到Spring容器中,然后通过注入等方式使用

  • Spring-Retry是通过捕获异常的方式来触发重试的,@Retryable标注方法产生的异常不能使用try-catch捕获,要在方法上抛出异常,不然不会触发重试

重试原则

  1. 查询可以进行重试
  2. 写操作要慎重,除非业务方支持重入

三.编程式使用Spring-Retry

3.1.核心类

  • RetryOperations : 定义了“重试”的基本框架(模板),要求传入RetryCallback,可选传入RecoveryCallback;
  • RetryCallback: 封装你需要重试的业务逻辑;
  • RecoverCallback:封装在多次重试都失败后"兜底"的业务逻辑;
  • RetryTemplate:RetryOperations的具体实现,组合了RetryListener[],BackOffPolicy,RetryPolicy。
  • RetryContext:重试下的上下文,可用于在多次Retry或者Retry 和Recover之间传递参数或状态;
  • RetryPolicy :重试的策略,可以固定次数的重试,也可以是指定超时时间进行重试;
  • BackOffPolicy:重试的等待策略,在业务逻辑执行发生异常时。如果需要重试,我们可能需要等一段时间(可能服务器过于繁忙,如果一直不间隔重试可能拖垮服务器),当然这段时间可以是 0,也可以是固定的,可以是随机的
  • RetryListener:典型的“监听者”,在重试的不同阶段通知“监听者”;

3.2.RetryOperations

RetryOperations是重试的顶级接口:

  • RetryOperations: 统一定义了重试的API(定义了实现“重试”的基本框架(模板))
  • RetryTemplate: 是RetryOperations模板模式实现,实现了重试和熔断etryTemplate将重试、提供健壮和不易出错的API供大家使用。

提供的API如下:
在这里插入图片描述

publicinterfaceRetryOperations{<T, EextendsThrowable> Texecute(RetryCallback<T, E> var1)throws E;<T, EextendsThrowable> Texecute(RetryCallback<T, E> var1, RecoveryCallback<T> var2)throws E;<T, EextendsThrowable> Texecute(RetryCallback<T, E> var1, RetryState var2)throws E, ExhaustedRetryException;<T, EextendsThrowable> Texecute(RetryCallback<T, E> var1, RecoveryCallback<T> var2, RetryState var3)throws E;}
  • RetryCallback:编写需要执行重试的业务逻辑,定义好业务逻辑后,就是如何重试的问题了。
  • RetryTemplate: 通过设置不同的重试策略来控制如何进行重试。默认的重试策略是SimpleRetryPlicy(会重试3次)

    第1次重试如果成功后面就不会继续重试了。如果3次都重试失败了流程结束或者返回兜底结果。而返回兜底结果需要配置RecoveyCallBack

  • RecoveyCallBack:从名字可以看出这是一个兜底回调接口,也就是重试失败后执行的逻辑。

    当重试超过最大重试时间或最大重试次数后可以调用RecoveryCallback进行恢复,比如返回假数据或托底数据。

3.3.RetryPolicy(重试策略)

RetryPolicy: 重试策略的顶级接口
在这里插入图片描述

publicinterfaceRetryPolicyextendsSerializable{booleancanRetry(RetryContext var1);

    RetryContextopen(RetryContext var1);voidclose(RetryContext var1);voidregisterThrowable(RetryContext var1, Throwable var2);}

方法说明:

  • canRetry:在每次重试的时候调用,是否可以继续重试的判断条件
  • open重试开始前调用,会创建一个重试上下文到RetryContext保存重试的堆栈等信息
  • registerThrowable每次重试异常时调用(有异常会继续重试)

以 SimpleRetryPolicy为例,当重试次数达到3(默认3次)停止重试,重试次数保存在重试上下文中

RetryPolicy提供了如下策略实现:

  • NeverRetryPolicy:只允许调用RetryCallback一次,不允许重试
  • AlwaysRetryPolicy:允许无限重试,直到成功,此方式逻辑不当会导致死循环
  • SimpleRetryPolicy固定次数重试策略,默认重试最大次数为3次RetryTemplate默认使用的策略
  • TimeoutRetryPolicy超时时间重试策略,默认超时时间为1秒,在指定的超时时间内允许重试
  • ExceptionClassifierRetryPolicy:设置不同异常的重试策略,类似组合重试策略,区别在于这里只区分不同异常的重试
  • CircuitBreakerRetryPolicy:有熔断功能的重试策略,需设置3个参数openTimeout、resetTimeout和delegate

    在这里插入图片描述

    • delegate:真正执行的重试策略,由构造方法传入,当重试失败时,则执行熔断策略,默认SimpleRetryPolicy策略
    • openTimeout:openWindow,熔断器电路打开的超时时间,当超过openTimeout之后熔断器电路变成半打开状态(主要有一次重试成功,则闭合电路),默认5000毫秒
    • resetTimeout:timeout,重置熔断器重新闭合的超时时间。默认20000毫秒
  • CompositeRetryPolicy组合重试策略,有两种组合方式

    乐观组合重试策略:指只要有一个策略允许重试即可以
    悲观组合重试策略:指只要有一个策略不允许重试即可以

    • 但不管哪种组合方式,组合中的每一个策略都会执行

3.4.BackOffPolicy(重试回退(等待)策略)

回退策略: 指的是每次重试是立即重试还是等待一段时间后重试默认情况下是立即重试,如果需要配置等待一段时间后重试则需要指定回退策略

比如是网络错误,立即重试将导致立即失败,最好等待一小段时间后重试,还要防止很多服务同时重试导致DDos。

  • BackOffPolicy是回退策略的顶级接口

BackOffPolicy 提供了如下策略实现
在这里插入图片描述

  • NoBackOffPolicy无等待策略,每次重试时立即重试

  • FixedBackOffPolicy固定时间的等待策略

    需设置参数sleeper和backOffPeriod

    • sleeper指定等待策略,默认是Thread.sleep,即线程休眠
    • backOffPeriod指定休眠时间,默认1000毫秒
  • UniformRandomBackOffPolicy随机时间回退策略

    需设置sleeper、minBackOffPeriod和maxBackOffPeriod
    该策略在[minBackOffPeriod,maxBackOffPeriod之间取一个随机休眠时间。

    • sleeper指定等待策略,默认是Thread.sleep,即线程休眠
    • minBackOffPeriod 默认500毫秒
    • maxBackOffPeriod 默认1500毫秒
  • ExponentialBackOffPolicy倍数等待策略

    需设置参数sleeper、initialInterval、maxInterval和multiplier

    • sleeper指定等待策略,默认是Thread.sleep,即线程休眠
    • initialInterval指定初始休眠时间,默认100毫秒
    • maxInterval指定最大休眠时间,默认30秒
    • multiplier指定乘数,即下一次休眠时间为当前休眠时间*multiplier,之前说过固定倍数可能会引起很多服务同时重试导致DDos,使用随机休眠时间来避免这种情况。
  • ExponentialRandomBackOffPolicy随机倍数等待策略,引入随机乘数可以实现随机乘数回退

3.5.RetryTemplate主要流程实现源码

RetryTemplate类是对RetryOperations的具体实现,组合了RetryListener[],BackOffPolicy,RetryPolicy

3.5.1.RetryTemplate.doExecute()方法

protected<T, EextendsThrowable> TdoExecute(RetryCallback<T, E> retryCallback,
      RecoveryCallback<T> recoveryCallback, RetryState state)throws E, ExhaustedRetryException{//重试策略
   RetryPolicy retryPolicy=this.retryPolicy;//退避策略
   BackOffPolicy backOffPolicy=this.backOffPolicy;//重试上下文,当前重试次数等都记录在上下文中
   RetryContext context=open(retryPolicy, state);try{//拦截器模式,执行RetryListener#openboolean running=doOpenInterceptors(retryCallback, context);//判断是否可以重试执行while(canRetry(retryPolicy, context)&&!context.isExhaustedOnly()){try{//执行RetryCallback回调return retryCallback.doWithRetry(context);}catch(Throwable e){//异常时,要进行下一次重试准备//遇到异常后,注册该异常的失败次数registerThrowable(retryPolicy, state, context, e);//执行RetryListener#onErrordoOnErrorInterceptors(retryCallback, context, e);//如果可以重试,执行退避算法,比如休眠一小段时间后再重试if(canRetry(retryPolicy, context)&&!context.isExhaustedOnly()){
               backOffPolicy.backOff(backOffContext);}//在有状态重试时,如果是需要执行回滚操作的异常,则立即抛出异常//shouldRethrow方法只有一行代码: state != null && state.rollbackFor(context.getLastThrowable())if(shouldRethrow(retryPolicy, context, state)){throw RetryTemplate.<E>wrapIfNecessary(e);}}//如果是有状态重试,且有GLOBAL_STATE属性,则立即跳出重试终止;当抛出的异常是非需要执行回滚操作的异常时,//才会执行到此处,CircuitBreakerRetryPolicy会在此跳出循环;if(state!= null&& context.hasAttribute(GLOBAL_STATE)){break;}}//重试失败后,如果有RecoveryCallback,则执行此回调,否则抛出异常returnhandleRetryExhausted(recoveryCallback, context, state);}catch(Throwable e){throw RetryTemplate.<E>wrapIfNecessary(e);}finally{//清理环境close(retryPolicy, context, state, lastException== null|| exhausted);//执行RetryListener#close,比如统计重试信息doCloseInterceptors(retryCallback, context, lastException);}}

3.5.2.如何使用RetryTemplate

@TestpublicvoidtestSimple()throws Exception{@DataclassFoo{private String id;}

        RetryTemplate template=newRetryTemplate();//超时时间重试策略,默认超时时间为1秒,在指定的超时时间内允许重试
        TimeoutRetryPolicy policy=newTimeoutRetryPolicy();
        policy.setTimeout(3000L);

        template.setRetryPolicy(policy);
        Foo result= template.execute(//可能触发重试的业务逻辑newRetryCallback<Foo, Exception>(){@Overridepublic FoodoWithRetry(RetryContext context){try{
                            System.out.println("调用百度接口。。。。");
                            TimeUnit.MILLISECONDS.sleep(500);thrownewRuntimeException("调用百度接口超时");}catch(InterruptedException e){//e.printStackTrace();}returnnewFoo();}},//重试耗尽后的回调,配置了RecoveryCallback,超出重试次数后不会抛出异常,而是执行回调里的代码newRecoveryCallback<Foo>(){//可以没有RecoveryCallback,超出重试次数后依然会抛出异常@Overridepublic Foorecover(RetryContext context)throws Exception{
                        System.out.println("调用百度接口。。。recover");returnnewFoo();}});

        System.out.println("result=>"+ result);}

我们模拟调用百度并将结果返回给用户,如果该调用失败,则重试该调用,直到达到超时为止。

如果不使用RecoveryCallback,当重试失败后,当重试失败后,抛出异常
在这里插入图片描述
如果使用RecoveryCallback,,当重试失败后,执行RecoveryCallback
在这里插入图片描述

3.6.状态重试 OR 无状态重试

3.6.1.无状态重试

无状态重试: 是在一个循环中执行完重试策略,即重试上下文保持在一个线程上下文中,在一次调用中进行完整的重试策略判断。

非常简单的情况,如远程调用某个查询方法时是最常见的无状态重试。SimpleRetryPolicy就属于无状态重试,因为重试是在一个循环中完成的。

publicstaticvoidmain(String[] args){
        RetryTemplate template=newRetryTemplate();//重试策略:次数重试策略
        RetryPolicy retryPolicy=newSimpleRetryPolicy(3);
        template.setRetryPolicy(retryPolicy);//退避策略:倍数回退策略
        ExponentialBackOffPolicy backOffPolicy=newExponentialBackOffPolicy();
        backOffPolicy.setInitialInterval(100);
        backOffPolicy.setMaxInterval(3000);
        backOffPolicy.setMultiplier(2);
        backOffPolicy.setSleeper(newThreadWaitSleeper());
        template.setBackOffPolicy(backOffPolicy);


        String resul= template.execute(newRetryCallback<String, RuntimeException>(){@Overridepublic StringdoWithRetry(RetryContext context)throws RuntimeException{
                        System.out.println(
  • 作者:墩墩分墩
  • 原文链接:https://blog.csdn.net/qq877728715/article/details/110039456
    更新时间:2022-09-05 10:08:36