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

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. 循环重试(包括:重试次数,重试间隔)

public class TestRetry {
    
    
    //最大重试次数
    private static final Integer tryTimes = 6;
    //重试间隔时间单位秒
    private static final Integer intervalTime = 1;
 
    public static void main(String[] args) throws InterruptedException {
    
    
        boolean flag = TestRetry.retryBuss();
        System.out.println("最终执行结果:" + (flag ? "成功" : "失败"));
    }
 
    public static boolean retryBuss() 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
     */
    private static boolean execute(int retryNum) {
    
    
        Random random = new Random();
        int a = random.nextInt(10);
        boolean flag = true;
        try {
    
    
            if (a != 6) {
    
    
                flag = false;
                throw new RuntimeException();
            }
        } 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
@EnableRetry
public class RetryConfiguration {
    
    
    @Bean
    public PayService payService() {
    
    
        return new PayService();
    }
}

3.实现类

  • 服务类一般写主要逻辑,在需要重试的方法上面使用@Retryable
//@Service
@Slf4j
public class PayService {
    
    
    private final int totalNum = 100000;
    
    @Retryable(value = {
    
    Exception.class}, maxAttempts = 3, backoff = @Backoff(delay = 2000, multiplier = 1.5))
    public int minGoodsNum(int num) {
    
    
        log.info("减库存开始=>" + LocalTime.now());
        log.info("库存=>" + totalNum);
        if (num <= 0) {
    
    
            throw new IllegalArgumentException("数量不对");
        }
        log.info("减库存执行结束=>" + LocalTime.now());
        return totalNum - num;
    }

    /**
     * 使用@Recover注解,当重试次数达到设置的次数的时候,还是失败抛出异常,执行的回调函数。
     */
    @Recover
    public int recover(Exception e) {
    
    
        log.warn("减库存失败!!!" + LocalTime.now());
        //记日志到数据库
        return totalNum;
    }
}

测试代码

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class RetryTest {
    
    
    @Autowired
    private PayService payService;

    /**
     * 应该使用 org.junit.Test 才是正确的
     */
    @Test
    public void minGoodsNum() {
    
    
        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如下:
在这里插入图片描述

public interface RetryOperations {
    
    
    <T, E extends Throwable> T execute(RetryCallback<T, E> var1) throws E;

    <T, E extends Throwable> T execute(RetryCallback<T, E> var1, RecoveryCallback<T> var2) throws E;

    <T, E extends Throwable> T execute(RetryCallback<T, E> var1, RetryState var2) throws E, ExhaustedRetryException;

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

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

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

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

3.3.RetryPolicy(重试策略)

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

public interface RetryPolicy extends Serializable {
    
    
    boolean canRetry(RetryContext var1);

    RetryContext open(RetryContext var1);

    void close(RetryContext var1);

    void registerThrowable(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, E extends Throwable> T doExecute(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#open
      boolean 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#onError
            doOnErrorInterceptors(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,则执行此回调,否则抛出异常
      return handleRetryExhausted(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

   @Test
    public void testSimple() throws Exception {
    
    
        @Data
        class Foo {
    
    
            private String id;
        }

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

        template.setRetryPolicy(policy);
        Foo result = template.execute(
                //可能触发重试的业务逻辑
                new RetryCallback<Foo, Exception>() {
    
    
                    @Override
                    public Foo doWithRetry(RetryContext context) {
    
    
                        try {
    
    
                            System.out.println("调用百度接口。。。。");
                            TimeUnit.MILLISECONDS.sleep(500);
                            throw new RuntimeException("调用百度接口超时");
                        } catch (InterruptedException e) {
    
    
                            //e.printStackTrace();
                        }
                        return new Foo();
                    }

                },
                //重试耗尽后的回调,配置了RecoveryCallback,超出重试次数后不会抛出异常,而是执行回调里的代码
                new RecoveryCallback<Foo>() {
    
    //可以没有RecoveryCallback,超出重试次数后依然会抛出异常
                    @Override
                    public Foo recover(RetryContext context) throws Exception {
    
    
                        System.out.println("调用百度接口。。。recover");
                        return new Foo();
                    }
                });

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

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

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

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

3.6.1.无状态重试

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

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

    public static void main(String[] args) {
    
    
        RetryTemplate template = new RetryTemplate();
        //重试策略:次数重试策略
        RetryPolicy retryPolicy = new SimpleRetryPolicy(3);
        template.setRetryPolicy(retryPolicy);

        //退避策略:倍数回退策略
        ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
        backOffPolicy.setInitialInterval(100);
        backOffPolicy.setMaxInterval(3000);
        backOffPolicy.setMultiplier(2);
        backOffPolicy.setSleeper(new ThreadWaitSleeper());
        template.setBackOffPolicy(backOffPolicy);


        String resul = template.execute(
                new RetryCallback<String, RuntimeException>() {
    
    
                    @Override
                    public String doWithRetry(RetryContext context) throws RuntimeException {
    
    
                        System.out.println("retry count:" + context.getRetryCount());
                        throw new RuntimeException("timeout");
                    }
                },
                new RecoveryCallback<String>() {
    
    
                    @Override
                    public String recover(RetryContext context) throws Exception {
    
    
                        return "default";
                    }
                });
        System.out.println("result=>" + result);
    }

在这里插入图片描述

3.6.2.有状态重试(回滚/熔断)

有状态重试:就是不在一个线程上下文完成重试,有2种场景需要使用有状态重试,事务操作需要回滚或者熔断器模式

  • 在事务操作需要回滚场景时,当整个操作中抛出的是数据库异常DataAccessException,则不能进行重试需要回滚,而抛出其他异常则可以进行重试,可以通过RetryState实现:
 @Test
    public void retryState () {
    
    
        RetryTemplate template = new RetryTemplate();
        //重试策略:次数重试策略
        RetryPolicy retryPolicy = new SimpleRetryPolicy(3);
        template.setRetryPolicy(retryPolicy);

        //当前状态的名称,当把状态放入缓存时,通过该key查询获取
        Object key = "mykey";
        //是否每次都重新生成上下文还是从缓存中查询,即全局模式(如熔断器策略时从缓存中查询)
        boolean isForceRefresh = true;
        //对DataAccessException进行回滚
        BinaryExceptionClassifier rollbackClassifier = new BinaryExceptionClassifier(Collections.<Class<? extends Throwable>>singleton(DataAccessException.class));

        RetryState state = new DefaultRetryState(key, isForceRefresh, rollbackClassifier);

        String result = template.execute(
                new RetryCallback<String, RuntimeException>() {
    
    
                    @Override
                    public String doWithRetry(RetryContext context) throws RuntimeException {
    
    
                        System.out.println("retry count:" + context.getRetryCount());
                        throw new TypeMismatchDataAccessException("");
                    }
                },
                new RecoveryCallback<String>() {
    
    
                    @Override
                    public String recover(RetryContext context) throws Exception {
    
    
                        System.out.println("recovery count:" + context.getRetryCount());
                        return "default";
                    }
                }, state);
    }

执行结果:报异常,没有进RecoveryCallback中

在这里插入图片描述
RetryTemplate中在有状态重试时,执行RecoveryCallback报异常

(截图为 RetryTemplate主要流程实现doExecute方法的源码)
在这里插入图片描述

什么是熔断?

  • 熔断的意思是指 不在当前循环中处理重试,而是全局重试模式(不是线程上下文)。
  • 熔断会跳出循环,那么必然会丢失线程上下文的堆栈信息。那么肯定需要一种“全局模式”保存这种信息,目前的实现放在一个缓存map实现的,下次从缓存中获取就能继续重试了。

熔断器场景

  • 在有状态重试时,且是全局模式时,跳出重试循环

测试代码

@Test
    public void circuitBreakerRetryPolicy () {
    
    
        RetryTemplate template = new RetryTemplate();
        //传入RetryPolicy(每个RetryPolicy实现都有自己的重试策略实现),是真正判断是否重试的策略,当重试失败时,则执行熔断策略;
        CircuitBreakerRetryPolicy retryPolicy = new CircuitBreakerRetryPolicy(new SimpleRetryPolicy(3));
        //熔断器电路打开的超时时间
        retryPolicy.setOpenTimeout(5000);
        //重置熔断器重新闭合的超时时间
        retryPolicy.setResetTimeout(20000);
        template.setRetryPolicy(retryPolicy);

        for (int i = 0; i < 10; i++) {
    
    
            try {
    
    
                Object key = "circuit";
                boolean isForceRefresh = false;
                RetryState state = new DefaultRetryState(key, isForceRefresh);

                String result = template.execute(
                		//重试逻辑
                        new RetryCallback<String, RuntimeException>() {
    
    
                            @Override
                            public String doWithRetry(RetryContext context) throws RuntimeException {
    
    
                                System.out.println("retry count:" + context.getRetryCount());
                                throw new RuntimeException("timeout");
                            }
                        },
                        //重试失败兜底
                        new RecoveryCallback<String>() {
    
    
                            @Override
                            public String recover(RetryContext context) throws Exception {
    
    
                                return "default";
                            }
                        }, state);
                System.out.println(result);
            } catch (Exception e) {
    
    
                System.out.println("catch=>" + e.getMessage());
            }
        }
    }

执行结果
在这里插入图片描述
为什么说是全局模式呢?

  • 我们配置了isForceRefresh为false,则在获取上下文时是根据key “circuit”从缓存中获取,从而拿到同一个上下文
    在这里插入图片描述
    如下RetryTemplate源码说明在有状态模式下,不会在循环中进行重试,会跳出循环
    在这里插入图片描述

熔断器策略CircuitBreakerRetryPolicy需要配置三个参数:

  • delegate:当前重试策略,由构造方法传入,当重试失败时,则执行熔断策略
  • openTimeout:熔断器电`路打开的超时时间,当超过openTimeout之后熔断器电路变成半打开状态(主要有一次重试成功,则闭合电路)

    源码的openWindow属性

  • resetTimeout:重置熔断器重新闭合的超时时间。

    源码的timeOut属性

CircuitBreakerRetryPolicy.isOpen()源码

public boolean isOpen() {
    
    
   long time = System.currentTimeMillis() - this.start;
   boolean retryable = this.policy.canRetry(this.context);
   if (!retryable) {
    
    //重试失败
      //在重置熔断器超时后,熔断器器电路闭合,重置上下文
      if (time > this.timeout) {
    
    
         this.context = createDelegateContext(policy, getParent());
         this.start = System.currentTimeMillis();
         retryable = this.policy.canRetry(this.context);
      } else if (time < this.openWindow) {
    
    
         //当在熔断器打开状态时,熔断器电路打开,立即熔断
         if ((Boolean) getAttribute(CIRCUIT_OPEN) == false) {
    
    
            setAttribute(CIRCUIT_OPEN, true);
         }
         this.start = System.currentTimeMillis();
         return true;
      }
   } else {
    
    //重试成功
      //在熔断器电路半打开状态时,断路器电路闭合,重置上下文
      if (time > this.openWindow) {
    
    
         this.start = System.currentTimeMillis();
         this.context = createDelegateContext(policy, getParent());
      }
   }
   setAttribute(CIRCUIT_OPEN, !retryable);
   return !retryable;
}

从如上代码可看出spring-retry的熔断策略相对简单:

  • 当重试失败,且在熔断器打开时间窗口[0,openWindow) 内,立即熔断;
  • 当重试失败,且在指定超时时间后(>timeout),熔断器电路重新闭合;
  • 在熔断器半打开状态[openWindow, timeout] 时,只要重试成功则重置上下文,断路器闭合。

使用CircuitBreakerRetryPolicy注意事项

  • CircuitBreakerRetryPolicy的delegate(重试策略)应该配置基于次数的SimpleRetryPolicy或者基于超时的TimeoutRetryPolicy策略,且策略都是全局模式,而非局部模式,所以要注意次数或超时的配置合理性

比如SimpleRetryPolicy配置为3次,openWindow=5s,timeout=20s,我们来CircuitBreakerRetryPolicy的极端情况。

3.6.3.通过RetryListener实现拦截器模式

Spring-retry通过RetryListener实现拦截器模式,默认提供了StatisticsListener实现重试操作统计分析数据

  • 要给RetryCallback定义一个name如“method.key”,从而查询该RetryCallback的统计分析数据
    @Test
    public void analyses() {
    
    
        RetryTemplate template = new RetryTemplate();
        DefaultStatisticsRepository repository = new DefaultStatisticsRepository();
        StatisticsListener listener = new StatisticsListener(repository);
        template.setListeners(new RetryListener[]{
    
    listener});
     
        for (int i = 0; i < 10; i++) {
    
    
            String result = template.execute(new RetryCallback<String, RuntimeException>() {
    
    
                @Override
                public String doWithRetry(RetryContext context) throws RuntimeException {
    
    
                    context.setAttribute(RetryContext.NAME, "method.key");
                    return "ok";
                }
            });
        }
        
        RetryStatistics statistics = repository.findOne("method.key");
        System.out.println(statistics);
    }

执行结果
在这里插入图片描述

3.7.案例实践

场景描述: num作为计数器,如果num小于5则抛出异常,num会进行自增一操作,直到等于5方正常返回,否则根据重试策略进行重试操作,如果直到最后一直未重试成功,则返回Integer最大值。在重试上下文中添加一个value变量,后续通过其值实现根据返回值判断重试应用。最后打印最终的返回值。
公共方法

3.7.0.公用代码

下面案例中调用的run方法就是下面这个

    /**
     * 运行重试方法
     *
     * @param retryTemplate
     * @throws Exception
     */
    public void run(RetryTemplate retryTemplate) throws Exception {
    
    
        Integer result = retryTemplate.execute(
                new RetryCallback<Integer, Exception>() {
    
    
                    int i = 0;

                    // 重试操作
                    @Override
                    public Integer doWithRetry(RetryContext retryContext) throws Exception {
    
    
                        retryContext.setAttribute("value", i);
                        log.info("重试 {} 次.", retryContext.getRetryCount());
                        return checkLen(i++);
                    }
                },
                new RecoveryCallback<Integer>() {
    
    //兜底回调
                    @Override
                    public Integer recover(RetryContext retryContext) throws Exception {
    
    
                        log.info("重试{}次后,调用兜底方法", retryContext.getRetryCount());
                        return Integer.MAX_VALUE;
                    }

                });

        log.info("最终结果: {}", result);
    }


    /**
     * 根据i判断是否抛出异常
     *
     * @param num
     * @return
     * @throws Exception
     */
    public int checkLen(int num) throws Exception {
    
    
        //小于5抛出异常
        if (num < 5) throw new Exception(num + " le 5");
        //否则正常返回
        return num;
    }

3.7.1.SimpleRetryPolicy固定次数重试策略

 @Test
    public void retryFixTimes() throws Exception {
    
    
        //重试模板
        RetryTemplate retryTemplate = new RetryTemplate();
        //简单重试策略
        SimpleRetryPolicy simpleRetryPolicy = new SimpleRetryPolicy();
        //最大重试次数3次
        simpleRetryPolicy.setMaxAttempts(3);
        //模板设置重试策略
        retryTemplate.setRetryPolicy(simpleRetryPolicy);
        //开始执行- 超过3次最大重试次数,触发了recoveryCall,并返回Integer最大值。
        run(retryTemplate);
    }

执行结果
在这里插入图片描述
超过3次最大重试次数,触发了recoveryCall,并返回Integer最大值

3.7.2.AlwaysRetryPolicy无限重试策略

AlwaysRetryPolicy 允许无限重试,直到成功,此方式逻辑不当会导致死循环

    @Test
    public void retryAlwaysTimes() throws Exception {
    
    
        //重试模板
        RetryTemplate retryTemplate = new RetryTemplate();
        //设置为无限重试策略
        retryTemplate.setRetryPolicy(new AlwaysRetryPolicy());
        //开始执行-直到i等于5则正常返回,之前将实现无限重试。
        run(retryTemplate);
    }

执行结果
在这里插入图片描述
直到i等于5则正常返回,之前将实现无限重试。

3.7.3.TimeoutRetryPolicy超时时间重试策略

@Test
    public void retryTimeout() throws Exception {
    
    
        //重试模板
        RetryTemplate retryTemplate = new RetryTemplate();

        TimeoutRetryPolicy timeoutRetryPolicy = new TimeoutRetryPolicy();
        //超时时间为 1000 毫秒
        timeoutRetryPolicy.setTimeout(1000);

        //固定时间的回退策略,需设置参数sleeper和backOffPeriod,sleeper指定等待策略,默认是Thread.sleep,即线程休眠,backOffPeriod指定休眠时间,默认1秒,单位毫秒
        FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
        //休眠时间400毫秒
        fixedBackOffPolicy.setBackOffPeriod(400);

        //设置为超时时间重试策略
        retryTemplate.setRetryPolicy(timeoutRetryPolicy);
        //设置为固定时间的回退策略
        retryTemplate.setBackOffPolicy(fixedBackOffPolicy);

        //开始执行-设定1000ms后则认定为超时,每次重试等待时长400ms,故重试3次后即会超出超时阈值,触发RecoveryCallback回调,并返回Integer最大值。
        run(retryTemplate);
    }

执行策略
在这里插入图片描述
设定1000ms后则认定为超时,每次重试等待时长400ms,故重试3次后即会超出超时阈值,触发RecoveryCallback回调,并返回Integer最大值。

3.7.4.根据返回结果值实现重试

    @Test
    public void retryWithResult() throws Exception {
    
    
        //重试模板
        RetryTemplate retryTemplate = new RetryTemplate();

        //设置为无限重试策略
        retryTemplate.setRetryPolicy(new AlwaysRetryPolicy() {
    
    
            private static final long serialVersionUID = 1213824522266301314L;

            @Override
            public boolean canRetry(RetryContext context) {
    
    
                //小于1则重试
                return context.getAttribute("value") == null || Integer.parseInt(context.getAttribute("value").toString()) < 1;
            }
        });

        //开始执行-如果value值小于1或者为null则进行重试,反之不在进行重试,触发RecoveryCallback回调,并返回Integer最大值。
        run(retryTemplate);
    }

在这里插入图片描述
如果value值小于1或者为null则进行重试,反之不在进行重试,触发RecoveryCallback回调,并返回Integer最大值。

3.7.5.启用熔断器重试策略

 @Test
    public void retryCircuitBreakerTest() {
    
    
        RetryTemplate retryTemplate = new RetryTemplate();

        //传入RetryPolicy(每个RetryPolicy实现都有自己的重试策略实现),是真正判断是否重试的策略,当重试失败时,则执行熔断策略;
        CircuitBreakerRetryPolicy retryPolicy = new CircuitBreakerRetryPolicy(new SimpleRetryPolicy(4));

        //固定时间等待策略-每次重试等待300毫秒
        FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
        fixedBackOffPolicy.setBackOffPeriod(300);

        // 熔断器电路打开的超时时间
        retryPolicy.setOpenTimeout(1500);
        //重置熔断器重新闭合的超时时间
        retryPolicy.setResetTimeout(2000);

        //设置重试策略
        retryTemplate.setRetryPolicy(retryPolicy);
        //设置回退策略
        retryTemplate.setBackOffPolicy(fixedBackOffPolicy);

        long startTime = System.currentTimeMillis();

        //
        IntStream.range(0, 10).forEach(index -> {
    
    
            try {
    
    
                Thread.sleep(100);
                RetryState state = new DefaultRetryState("circuit", false);
                String result = retryTemplate.execute(
                        //重试业务逻辑
                        new RetryCallback<String, RuntimeException>() {
    
    
                            @Override
                            public String doWithRetry(RetryContext context) throws RuntimeException {
    
    
                                log.info("重试 {} 次", context.getRetryCount());
                                if (System.currentTimeMillis() - startTime > 1300 && System.currentTimeMillis() - startTime < 1500) {
    
    
                                    return "retryCallback-success";
                                }
                                throw new RuntimeException("timeout");
                            }
                        },
                        //重试失败回调
                        new RecoveryCallback<String>() {
    
    
                            @Override
                            public String recover(RetryContext context) throws Exception {
    
    
                                return "recoveryCallback-default";
                            }
                        }, state);
                log.info("result: {}", result);
            } catch (Exception e) {
    
    
                log.error("报错了: type:{}:{}", e.getClass().getName(), e.getMessage());
            }
        });
    }

执行结果
在这里插入图片描述

设定重试次数为4次,在执行1300ms至1500ms期间连续两次调用成功,无需重试,其后继续抛出异常重试,第4次重试时(1405)仍然在1500ms内,故打开了断路器,后续请求异常均会直接返回 RecoveryCallback中回调定义。

3.8.spring-retry1.3 以上构建RetryTemplate

以下代码需要引入spring-retry1.3 以上才能使用,目前仅供参考

RetryTemplate template = RetryTemplate.builder()
                .maxAttempts(3)//重试次数
                .fixedBackoff(1000)//重试时间,单位ms
                .retryOn(RemoteAccessException.class)//触发重试异常
                .build();

template.execute(ctx -> {
    
    
    // ... do something
});

1.3开始可以这样构建

RetryTemplate.builder()
      .maxAttempts(10)
      .exponentialBackoff(100, 2, 10000)
      .retryOn(IOException.class)
      .traversingCauses()
      .build();
 
RetryTemplate.builder()
      .fixedBackoff(10)
      .withinMillis(3000)
      .build();
 
RetryTemplate.builder()
      .infiniteRetry()
      .retryOn(IOException.class)
      .uniformRandomBackoff(1000, 3000)
      .build();

3.9.较常见的重试技术实现

  1. Spring Retry重试框架;
  2. Guava Retry重试框架;
    Guava-Retry教程
  3. Spring Cloud 重试配置;

spring-retry重试与熔断详解—《亿级流量》内容补充

猜你喜欢

转载自blog.csdn.net/qq877728715/article/details/110039456