`
234390216
  • 浏览: 10192322 次
  • 性别: Icon_minigender_1
  • 来自: 深圳
博客专栏
A5ee55b9-a463-3d09-9c78-0c0cf33198cd
Oracle基础
浏览量:460747
Ad26f909-6440-35a9-b4e9-9aea825bd38e
springMVC介绍
浏览量:1771676
Ce363057-ae4d-3ee1-bb46-e7b51a722a4b
Mybatis简介
浏览量:1395339
Bdeb91ad-cf8a-3fe9-942a-3710073b4000
Spring整合JMS
浏览量:393855
5cbbde67-7cd5-313c-95c2-4185389601e7
Ehcache简介
浏览量:678187
Cc1c0708-ccc2-3d20-ba47-d40e04440682
Cas简介
浏览量:529242
51592fc3-854c-34f4-9eff-cb82d993ab3a
Spring Securi...
浏览量:1178648
23e1c30e-ef8c-3702-aa3c-e83277ffca91
Spring基础知识
浏览量:461678
4af1c81c-eb9d-365f-b759-07685a32156e
Spring Aop介绍
浏览量:150089
2f926891-9e7a-3ce2-a074-3acb2aaf2584
JAXB简介
浏览量:66813
社区版块
存档分类
最新评论

Spring(34)——Spring Retry介绍

阅读更多

Spring Retry介绍

Spring retry是Spring提供的一种重试机制的解决方案。它内部抽象了一个RetryOperations接口,其定义如下。

public interface RetryOperations {

  <T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback) throws E;

  <T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback, RecoveryCallback<T> recoveryCallback) throws E;

  <T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback, RetryState retryState) throws E, ExhaustedRetryException;

  <T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback, RecoveryCallback<T> recoveryCallback, RetryState retryState)
          throws E;

}

从定义中可以看到它定义了几个重载的execute(),它们之间的差别就在于RetryCallback、RecoveryCallback、RetryState,其中核心参数是RetryCallback。RetryCallback的定义如下,从定义中可以看到它就定义了一个doWithRetry(),该方法的返回值就是RetryOperations的execute()的返回值,RetryCallback的范型参数中定义的Throwable是中执行可重试方法时可抛出的异常,可由外部进行捕获。

public interface RetryCallback<T, E extends Throwable> {

  T doWithRetry(RetryContext context) throws E;
  
}

当RetryCallback不能再重试的时候,如果定义了RecoveryCallback,就会调用RecoveryCallback,并以其返回结果作为execute()的返回结果。其定义如下。RetryCallback和RecoverCallback定义的接口方法都可以接收一个RetryContext参数,通过它可以获取到尝试次数,也可以通过其setAttribute()getAttribute()来传递一些信息。

public interface RecoveryCallback<T> {

  T recover(RetryContext context) throws Exception;

}

Spring Retry包括有状态的重试和无状态的重试,对于有状态的重试,它主要用来提供一个用于在RetryContextCache中保存RetryContext的Key,这样可以在多次不同的调用中应用同一个RetryContext(无状态的重试每次发起调用都是一个全新的RetryContext,在整个重试过程中是一个RetryContext,其不会进行保存。有状态的重试因为RetryContext是保存的,其可以跨或不跨线程在多次execute()调用中应用同一个RetryContext)。

使用Spring Retry需要引入如下依赖。

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
    <version>1.2.4.RELEASE</version>
</dependency>

Spring Retry提供了一个RetryOperations的实现,RetryTemplate,通过它我们可以发起一些可重试的请求。其内部的重试机制通过RetryPolicy来控制。RetryTemplate默认使用的是SimpleRetryPolicy实现,SimpleRetryPolicy只是简单的控制尝试几次,包括第一次调用。RetryTemplate默认使用的是尝试3次的策略。所以下面的单元策略是可以通过的,第一二次尝试都失败,此时counter变为3了,第3次尝试成功了,counter变为4了。

@Test
public void test() {
  RetryTemplate retryTemplate = new RetryTemplate();
  AtomicInteger counter = new AtomicInteger();
  RetryCallback<Integer, IllegalStateException> retryCallback = retryContext -> {
    if (counter.incrementAndGet() < 3) {//内部默认重试策略是最多尝试3次,即最多重试两次。
      throw new IllegalStateException();
    }
    return counter.incrementAndGet();
  };
  Integer result = retryTemplate.execute(retryCallback);

  Assert.assertEquals(4, result.intValue());
}

接着来看一个使用RecoveryCallback的例子。我们把上面的例子简单改了下,改为调用包含RecoveryCallback入参的execute(),RetryCallback内部也改为了即使尝试了3次后仍然会失败。此时将转为调用RecoveryCallback,RecoveryCallback内部通过RetryContext获取了尝试次数,此时RetryCallback已经尝试3次了,所以RetryContext获取的尝试次数是3,RecoveryCallback的返回结果30将作为execute()的返回结果。

@Test
public void testRecoveryCallback() {

  RetryTemplate retryTemplate = new RetryTemplate();
  AtomicInteger counter = new AtomicInteger();
  RetryCallback<Integer, IllegalStateException> retryCallback = retryContext -> {
    //内部默认重试策略是最多尝试3次,即最多重试两次。还不成功就会抛出异常。
    if (counter.incrementAndGet() < 10) {
      throw new IllegalStateException();
    }
    return counter.incrementAndGet();
  };

  RecoveryCallback<Integer> recoveryCallback = retryContext -> {
    //返回的应该是30。RetryContext.getRetryCount()记录的是尝试的次数,一共尝试了3次。
    return retryContext.getRetryCount() * 10;
  };
  //尝试策略已经不满足了,将不再尝试的时候会抛出异常。此时如果指定了RecoveryCallback将执行RecoveryCallback,
  //然后获得返回值。
  Integer result = retryTemplate.execute(retryCallback, recoveryCallback);

  Assert.assertEquals(30, result.intValue());
}

 

RetryPolicy

RetryTemplate内部的重试策略是由RetryPolicy控制的。RetryPolicy的定义如下。

public interface RetryPolicy extends Serializable {

  /**
   * @param context the current retry status
   * @return true if the operation can proceed
   */
  boolean canRetry(RetryContext context);

  /**
   * Acquire resources needed for the retry operation. The callback is passed
   * in so that marker interfaces can be used and a manager can collaborate
   * with the callback to set up some state in the status token.
   * @param parent the parent context if we are in a nested retry.
   *
   * @return a {@link RetryContext} object specific to this policy.
   *
   */
  RetryContext open(RetryContext parent);

  /**
   * @param context a retry status created by the
   * {@link #open(RetryContext)} method of this policy.
   */
  void close(RetryContext context);

  /**
   * Called once per retry attempt, after the callback fails.
   *
   * @param context the current status object.
   * @param throwable the exception to throw
   */
  void registerThrowable(RetryContext context, Throwable throwable);

}

 

SimpleRetryPolicy

RetryTemplate内部默认时候用的是SimpleRetryPolicy。SimpleRetryPolicy默认将对所有异常进行尝试,最多尝试3次。如果需要调整使用的RetryPolicy,可以通过RetryTemplate的setRetryPolicy()进行设置。比如下面代码就显示的设置了需要使用的RetryPolicy是不带参数的SimpleRetryPolicy,其默认会尝试3次。

public void testSimpleRetryPolicy() {
  RetryPolicy retryPolicy = new SimpleRetryPolicy();
  RetryTemplate retryTemplate = new RetryTemplate();
  retryTemplate.setRetryPolicy(retryPolicy);
  AtomicInteger counter = new AtomicInteger();
  Integer result = retryTemplate.execute(retryContext -> {
    if (counter.incrementAndGet() < 3) {
      throw new IllegalStateException();
    }
    return counter.get();
  });
  Assert.assertEquals(3, result.intValue());
}

如果希望最多尝试10次,只需要传入构造参数10即可,比如下面这样。

RetryPolicy retryPolicy = new SimpleRetryPolicy(10);

在实际使用的过程中,可能你不会希望所有的异常都进行重试,因为有的异常重试是解决不了问题的。所以可能你会想要指定可以重试的异常类型。通过SimpleRetryPolicy的构造参数可以指定哪些异常是可以进行重试的。比如下面代码我们指定了最多尝试10次,且只有IllegalStateException是可以进行重试的。那么在运行下面代码时前三次抛出的IllegalStateException都会再次进行尝试,第四次会抛出IllegalArgumentException,此时不能继续尝试了,该异常将会对外抛出。

@Test
public void testSimpleRetryPolicy() {
  Map<Class<? extends Throwable>, Boolean> retryableExceptions = Maps.newHashMap();
  retryableExceptions.put(IllegalStateException.class, true);
  RetryPolicy retryPolicy = new SimpleRetryPolicy(10, retryableExceptions);
  RetryTemplate retryTemplate = new RetryTemplate();
  retryTemplate.setRetryPolicy(retryPolicy);
  AtomicInteger counter = new AtomicInteger();
  retryTemplate.execute(retryContext -> {
    if (counter.incrementAndGet() < 3) {
      throw new IllegalStateException();
    } else if (counter.incrementAndGet() < 6) {
      throw new IllegalArgumentException();
    }
    return counter.get();
  });
}

看到这里可能你会有疑问,可以进行重试的异常定义为什么使用的是Map结构,而不是简单的通过Set或List来定义可重试的所有异常类似,而要多一个Boolean类型的Value来定义该异常是否可重试。这样做的好处是它可以实现包含/排除的逻辑,比如下面这样,我们可以指定对所有的RuntimeException都是可重试的,唯独IllegalArgumentException是一个例外。所以当你运行如下代码时其最终结果还是抛出IllegalArgumentException。

@Test
public void testSimpleRetryPolicy() {
  Map<Class<? extends Throwable>, Boolean> retryableExceptions = Maps.newHashMap();
  retryableExceptions.put(RuntimeException.class, true);
  retryableExceptions.put(IllegalArgumentException.class, false);
  RetryPolicy retryPolicy = new SimpleRetryPolicy(10, retryableExceptions);
  RetryTemplate retryTemplate = new RetryTemplate();
  retryTemplate.setRetryPolicy(retryPolicy);
  AtomicInteger counter = new AtomicInteger();
  retryTemplate.execute(retryContext -> {
    if (counter.incrementAndGet() < 3) {
      throw new IllegalStateException();
    } else if (counter.incrementAndGet() < 6) {
      throw new IllegalArgumentException();
    }
    return counter.get();
  });
}

SimpleRetryPolicy在判断一个异常是否可重试时,默认会取最后一个抛出的异常。我们通常可能在不同的业务层面包装不同的异常,比如有些场景我们可能需要把捕获到的异常都包装为BusinessException,比如说把一个IllegalStateException包装为BusinessException。我们程序中定义了所有的IllegalStateException是可以进行重试的,如果SimpleRetryPolicy直接取的最后一个抛出的异常会取到BusinessException。这可能不是我们想要的,此时可以通过构造参数traverseCauses指定可以遍历异常栈上的每一个异常进行判断。比如下面代码,在traverseCauses=false时,只会在抛出IllegalStateException时尝试3次,第四次抛出的Exception不是RuntimeException,所以不会进行重试。指定了traverseCauses=true时第四次尝试时抛出的Exception,再往上找时会找到IllegalArgumentException,此时又可以继续尝试,所以最终执行后counter的值会是6。

@Test
public void testSimpleRetryPolicy() throws Exception {
  Map<Class<? extends Throwable>, Boolean> retryableExceptions = Maps.newHashMap();
  retryableExceptions.put(RuntimeException.class, true);
  RetryPolicy retryPolicy = new SimpleRetryPolicy(10, retryableExceptions, true);
  RetryTemplate retryTemplate = new RetryTemplate();
  retryTemplate.setRetryPolicy(retryPolicy);
  AtomicInteger counter = new AtomicInteger();
  retryTemplate.execute(retryContext -> {
    if (counter.incrementAndGet() < 3) {
      throw new IllegalStateException();
    } else if (counter.incrementAndGet() < 6) {
      try {
        throw new IllegalArgumentException();
      } catch (Exception e) {
        throw new Exception(e);
      }
    }
    return counter.get();
  });
}

SimpleRetryPolicy除了前面介绍的3个构造方法外,还有如下这样一个构造方法,它的第四个参数表示当抛出的异常是在retryableExceptions中没有定义是否需要尝试时其默认的值,该值为true则表示默认可尝试。

public SimpleRetryPolicy(int maxAttempts, Map<Class<? extends Throwable>, Boolean> retryableExceptions,
                         boolean traverseCauses, boolean defaultValue)

下面代码中通过retryableExceptions指定了抛出IllegalFormatException时不进行重试,然后通过SimpleRetryPolicy的第四个参数指定了其它异常默认是可以进行重试的。所以下面的代码也可以正常运行,运行结束后counter的值是6。

@Test
public void testSimpleRetryPolicy() throws Exception {
  Map<Class<? extends Throwable>, Boolean> retryableExceptions = Maps.newHashMap();
  retryableExceptions.put(IllegalFormatException.class, false);
  RetryPolicy retryPolicy = new SimpleRetryPolicy(10, retryableExceptions, false, true);
  RetryTemplate retryTemplate = new RetryTemplate();
  retryTemplate.setRetryPolicy(retryPolicy);
  AtomicInteger counter = new AtomicInteger();
  retryTemplate.execute(retryContext -> {
    if (counter.incrementAndGet() < 3) {
      throw new IllegalStateException();
    } else if (counter.incrementAndGet() < 6) {
      throw new IllegalArgumentException();
    }
    return counter.get();
  });
}

 

AlwaysRetryPolicy

顾名思义就是一直重试,直到成功为止。所以对于下面代码而言,其会一直尝试100次,第100次的时候它就成功了。

@Test
public void testRetryPolicy() {
  RetryPolicy retryPolicy = new AlwaysRetryPolicy();
  RetryTemplate retryTemplate = new RetryTemplate();
  retryTemplate.setRetryPolicy(retryPolicy);
  AtomicInteger counter = new AtomicInteger();
  Integer result = retryTemplate.execute(retryContext -> {
    if (counter.incrementAndGet() < 100) {
      throw new IllegalStateException();
    }
    return counter.get();
  });
  Assert.assertEquals(100, result.intValue());
}

 

NeverRetryPolicy

与AlwaysRetryPolicy相对的一个极端是从不重试,NeverRetryPolicy的策略就是从不重试,但是第一次调用还是会发生的。所以对于下面代码而言,如果第一次获取的随机数不是3的倍数,则可以正常执行,否则将抛出IllegalStateException。

@Test
public void testRetryPolicy() {
  RetryPolicy retryPolicy = new NeverRetryPolicy();
  RetryTemplate retryTemplate = new RetryTemplate();
  retryTemplate.setRetryPolicy(retryPolicy);
  retryTemplate.execute(retryContext -> {
    int value = new Random().nextInt(100);
    if (value % 3 == 0) {
      throw new IllegalStateException();
    }
    return value;
  });
}

 

TimeoutRetryPolicy

TimeoutRetryPolicy用于在指定时间范围内进行重试,直到超时为止,默认的超时时间是1000毫秒。

@Test
public void testRetryPolicy() throws Exception {
  TimeoutRetryPolicy retryPolicy = new TimeoutRetryPolicy();
  retryPolicy.setTimeout(2000);//不指定时默认是1000
  RetryTemplate retryTemplate = new RetryTemplate();
  retryTemplate.setRetryPolicy(retryPolicy);
  AtomicInteger counter = new AtomicInteger();
  Integer result = retryTemplate.execute(retryContext -> {
    if (counter.incrementAndGet() < 10) {
      TimeUnit.MILLISECONDS.sleep(20);
      throw new IllegalStateException();
    }
    return counter.get();
  });
  Assert.assertEquals(10, result.intValue());
}

 

ExceptionClassifierRetryPolicy

之前介绍的SimpleRetryPolicy可以基于异常来判断是否需要进行重试。如果你需要基于不同的异常应用不同的重试策略怎么办呢?ExceptionClassifierRetryPolicy可以帮你实现这样的需求。下面的代码中我们就指定了当捕获的是IllegalStateException时将最多尝试5次,当捕获的是IllegalArgumentException时将最多尝试4次。其执行结果最终是抛出IllegalArgumentException的,但是在最终抛出IllegalArgumentException时counter的值是多少呢?换句话说它一共尝试了几次呢?答案是8次。按照笔者的写法,进行第5次尝试时不会抛出IllegalStateException,而是抛出IllegalArgumentException,它对于IllegalArgumentException的重试策略而言是第一次尝试,之后会再尝试3次,5+3=8,所以counter的最终的值是8。

@Test
public void testRetryPolicy() throws Exception {
  ExceptionClassifierRetryPolicy retryPolicy = new ExceptionClassifierRetryPolicy();

  Map<Class<? extends Throwable>, RetryPolicy> policyMap = Maps.newHashMap();
  policyMap.put(IllegalStateException.class, new SimpleRetryPolicy(5));
  policyMap.put(IllegalArgumentException.class, new SimpleRetryPolicy(4));
  retryPolicy.setPolicyMap(policyMap);

  RetryTemplate retryTemplate = new RetryTemplate();
  retryTemplate.setRetryPolicy(retryPolicy);
  AtomicInteger counter = new AtomicInteger();
  retryTemplate.execute(retryContext -> {
    if (counter.incrementAndGet() < 5) {
      throw new IllegalStateException();
    } else if (counter.get() < 10) {
      throw new IllegalArgumentException();
    }
    return counter.get();
  });
}

 

CircuitBreakerRetryPolicy

CircuitBreakerRetryPolicy是包含了断路器功能的RetryPolicy,它内部默认包含了一个SimpleRetryPolicy,最多尝试3次。在固定的时间窗口内(默认是20秒)如果底层包含的RetryPolicy的尝试次数都已经耗尽了,则其会打开断路器,默认打开时间是5秒,在这段时间内如果还有其它请求过来就不会再进行调用了。CircuitBreakerRetryPolicy需要跟RetryState一起使用,下面的代码中RetryTemplate使用的是CircuitBreakerRetryPolicy,一共调用了5次execute(),每次调用RetryCallback都会抛出IllegalStateException,并且会打印counter的当前值,前三次RetryCallback都是可以运行的,之后断路器打开了,第四五次执行execute()时就不会再执行RetryCallback了,所以你只能看到只进行了3次打印。

@Test
public void testCircuitBreakerRetryPolicy() throws Exception {
  CircuitBreakerRetryPolicy retryPolicy = new CircuitBreakerRetryPolicy();
  RetryTemplate retryTemplate = new RetryTemplate();
  retryTemplate.setRetryPolicy(retryPolicy);
  AtomicInteger counter = new AtomicInteger();
  RetryState retryState = new DefaultRetryState("key");
  for (int i=0; i<5; i++) {
    try {
      retryTemplate.execute(retryContext -> {
        System.out.println(LocalDateTime.now() + "----" + counter.get());
        TimeUnit.MILLISECONDS.sleep(100);
        if (counter.incrementAndGet() > 0) {
          throw new IllegalStateException();
        }
        return 1;
      }, null, retryState);
    } catch (Exception e) {
      
    }
  }
}

断路器默认打开的时间是5秒,5秒之后断路器又会关闭,RetryCallback又可以正常调用了。判断断路器是否需要打开的时间窗口默认是20秒,即在20秒内所有的尝试次数都用完了,就会打开断路器。如果在20秒内只尝试了两次(默认3次),则在新的时间窗口内尝试次数又将从0开始计算。可以通过如下方式进行这两个时间的设置。

  SimpleRetryPolicy delegate = new SimpleRetryPolicy(5);
  //底层允许最多尝试5次
  CircuitBreakerRetryPolicy retryPolicy = new CircuitBreakerRetryPolicy(delegate);
  retryPolicy.setOpenTimeout(2000);//断路器打开的时间
  retryPolicy.setResetTimeout(15000);//时间窗口

 

CompositeRetryPolicy

CompositeRetryPolicy可以用来组合多个RetryPolicy,可以设置必须所有的RetryPolicy都是可以重试的时候才能进行重试,也可以设置只要有一个RetryPolicy可以重试就可以进行重试。默认是必须所有的RetryPolicy都可以重试才能进行重试。下面代码中应用的就是CompositeRetryPolicy,它组合了两个RetryPolicy,最多尝试5次的SimpleRetryPolicy和超时时间是2秒钟的TimeoutRetryPolicy,所以它们的组合就是必须尝试次数不超过5次且尝试时间不超过2秒钟才能进行重试。execute()中执行的RetryCallback的逻辑是counter的值小于10时就抛出IllegalStateException,否则就返回counter的值。第一次尝试的时候会失败,第二次也是,直到第5次尝试也还是失败的,此时SimpleRetryPolicy已经不能再尝试了,而TimeoutRetryPolicy此时还是可以尝试的,但是由于前者已经不能再尝试了,所以整体就不能再尝试了。所以下面的执行会以抛出IllegalStateException告终。

@Test
public void testCompositeRetryPolicy() {
  CompositeRetryPolicy compositeRetryPolicy = new CompositeRetryPolicy();
  RetryPolicy policy1 = new SimpleRetryPolicy(5);
  TimeoutRetryPolicy policy2 = new TimeoutRetryPolicy();
  policy2.setTimeout(2000);
  RetryPolicy[] policies = new RetryPolicy[]{policy1, policy2};
  compositeRetryPolicy.setPolicies(policies);

  RetryTemplate retryTemplate = new RetryTemplate();
  retryTemplate.setRetryPolicy(compositeRetryPolicy);
  AtomicInteger counter = new AtomicInteger();
  retryTemplate.execute(retryContext -> {
    if (counter.incrementAndGet() < 10) {
      throw new IllegalStateException();
    }
    return counter.get();
  });
}

CompositeRetryPolicy也支持组合的RetryPolicy中只要有一个RetryPolicy满足条件就可以进行重试,这是通过参数optimistic控制的,默认是false,改为true即可。比如下面设置了setOptimistic(true),那么中尝试5次后SimpleRetryPolicy已经不满足了,但是TimeoutRetryPolicy还满足条件,所以最终会一直尝试,直到counter的值为10。

@Test
public void testCompositeRetryPolicy() {
  CompositeRetryPolicy compositeRetryPolicy = new CompositeRetryPolicy();
  RetryPolicy policy1 = new SimpleRetryPolicy(5);
  TimeoutRetryPolicy policy2 = new TimeoutRetryPolicy();
  policy2.setTimeout(2000);
  RetryPolicy[] policies = new RetryPolicy[]{policy1, policy2};
  compositeRetryPolicy.setPolicies(policies);
  compositeRetryPolicy.setOptimistic(true);

  RetryTemplate retryTemplate = new RetryTemplate();
  retryTemplate.setRetryPolicy(compositeRetryPolicy);
  AtomicInteger counter = new AtomicInteger();
  Integer result = retryTemplate.execute(retryContext -> {
    if (counter.incrementAndGet() < 10) {
      throw new IllegalStateException();
    }
    return counter.get();
  });
  Assert.assertEquals(10, result.intValue());
}

 

BackOffPolicy

BackOffPolicy用来定义在两次尝试之间需要间隔的时间,RetryTemplate内部默认使用的是NoBackOffPolicy,其在两次尝试之间不会进行任何的停顿。对于一般可重试的操作往往是基于网络进行的远程请求,它可能由于网络波动暂时不可用,如果立马进行重试它可能还是不可用,但是停顿一下,过一会再试可能它又恢复正常了,所以在RetryTemplate中使用BackOffPolicy往往是很有必要的。

 

FixedBackOffPolicy

FixedBackOffPolicy将在两次重试之间进行一次固定的时间间隔,默认是1秒钟,也可以通过setBackOffPeriod()进行设置。下面代码中指定了两次重试的时间间隔是1秒钟,第一次尝试会失败,等一秒后会进行第二次尝试,第二次尝试会成功。

@Test
public void testFixedBackOffPolicy() {

  FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
  backOffPolicy.setBackOffPeriod(1000);
  RetryTemplate retryTemplate = new RetryTemplate();
  retryTemplate.setBackOffPolicy(backOffPolicy);

  long t1 = System.currentTimeMillis();
  long t2 = retryTemplate.execute(retryContext -> {
    if (System.currentTimeMillis() - t1 < 1000) {
      throw new IllegalStateException();
    }
    return System.currentTimeMillis();
  });
  Assert.assertTrue(t2 - t1 > 1000);
  Assert.assertTrue(t2 - t1 < 1100);
}

 

ExponentialBackOffPolicy

ExponentialBackOffPolicy可以使每一次尝试的间隔时间都不一样,它有3个重要的参数,初始间隔时间、后一次间隔时间相对于前一次间隔时间的倍数和最大的间隔时间,它们的默认值分别是100毫秒、2.0和30秒。下面的代码使用了ExponentialBackOffPolicy,指定了初始间隔时间是1000毫秒,每次间隔时间以2倍的速率递增,最大的间隔时间是5000毫秒,它最多可以尝试10次。所以当第1次尝试失败后会间隔1秒后进行第2次尝试,之后再间隔2秒进行第3次尝试,之后再间隔4秒进行第4次尝试,之后都是间隔5秒再进行下一次尝试,因为再翻倍已经超过了设定的最大的间隔时间。

@Test
public void testExponentialBackOffPolicy() {
  ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
  backOffPolicy.setInitialInterval(1000);
  backOffPolicy.setMaxInterval(5000);
  backOffPolicy.setMultiplier(2.0);
  RetryTemplate retryTemplate = new RetryTemplate();
  retryTemplate.setBackOffPolicy(backOffPolicy);
  int maxAttempts = 10;
  retryTemplate.setRetryPolicy(new SimpleRetryPolicy(maxAttempts));

  long t1 = System.currentTimeMillis();
  long t2 = retryTemplate.execute(retryContext -> {
    if (retryContext.getRetryCount() < maxAttempts-1) {//最后一次尝试会成功
      throw new IllegalStateException();
    }
    return System.currentTimeMillis();
  });
  long time = 0 + 1000 + 1000 * 2 + 1000 * 2 * 2 + 5000 * (maxAttempts - 4);
  Assert.assertTrue((t2-t1) - time < 100);
}

 

ExponentialRandomBackOffPolicy

ExponentialRandomBackOffPolicy的用法跟ExponentialBackOffPolicy的用法是一样的,它继承自ExponentialBackOffPolicy,在确定间隔时间时会先按照ExponentialBackOffPolicy的方式确定一个时间间隔,然后再随机的增加一个0-1的。比如取得的随机数是0.1即表示增加10%,每次需要确定重试间隔时间时都会产生一个新的随机数。如果指定的初始间隔时间是100毫秒,增量倍数是2,最大间隔时间是2000毫秒,则按照ExponentialBackOffPolicy的重试间隔是100、200、400、800,而ExponentialRandomBackOffPolicy产生的间隔时间可能是111、256、421、980。下面代码使用了ExponentialRandomBackOffPolicy,它打印出了每次重试时间的间隔。如果你有兴趣,你运行它会看到它会在ExponentialBackOffPolicy的基础上每次都随机的增长0-1倍。

@Test
public void testExponentialRandomBackOffPolicy() {
  ExponentialRandomBackOffPolicy backOffPolicy = new ExponentialRandomBackOffPolicy();
  backOffPolicy.setInitialInterval(1000);
  backOffPolicy.setMaxInterval(5000);
  backOffPolicy.setMultiplier(2.0);
  RetryTemplate retryTemplate = new RetryTemplate();
  retryTemplate.setBackOffPolicy(backOffPolicy);
  int maxAttempts = 10;
  retryTemplate.setRetryPolicy(new SimpleRetryPolicy(maxAttempts));

  String lastAttemptTime = "lastAttemptTime";
  retryTemplate.execute(retryContext -> {
    if (retryContext.hasAttribute(lastAttemptTime)) {
      System.out.println(System.currentTimeMillis() - (Long) retryContext.getAttribute(lastAttemptTime));
    }
    retryContext.setAttribute(lastAttemptTime, System.currentTimeMillis());
    if (retryContext.getRetryCount() < maxAttempts-1) {//最后一次尝试会成功
      throw new IllegalStateException();
    }
    return System.currentTimeMillis();
  });
}

 

UniformRandomBackOffPolicy

UniformRandomBackOffPolicy用来每次都随机的产生一个间隔时间,默认的间隔时间是在500-1500毫秒之间。可以通过setMinBackOffPeriod()设置最小间隔时间,通过setMaxBackOffPeriod()设置最大间隔时间。

@Test
public void testUniformRandomBackOffPolicy() {
  UniformRandomBackOffPolicy backOffPolicy = new UniformRandomBackOffPolicy();
  backOffPolicy.setMinBackOffPeriod(1000);
  backOffPolicy.setMaxBackOffPeriod(3000);
  RetryTemplate retryTemplate = new RetryTemplate();
  retryTemplate.setBackOffPolicy(backOffPolicy);
  int maxAttempts = 10;
  retryTemplate.setRetryPolicy(new SimpleRetryPolicy(maxAttempts));

  String lastAttemptTime = "lastAttemptTime";
  retryTemplate.execute(retryContext -> {
    if (retryContext.hasAttribute(lastAttemptTime)) {
      System.out.println(System.currentTimeMillis() - (Long) retryContext.getAttribute(lastAttemptTime));
    }
    retryContext.setAttribute(lastAttemptTime, System.currentTimeMillis());
    if (retryContext.getRetryCount() < maxAttempts-1) {//最后一次尝试会成功
      throw new IllegalStateException();
    }
    return System.currentTimeMillis();
  });
}

 

监听器

RetryTemplate中可以注册一些RetryListener,它可以用来对整个Retry过程进行监听。RetryListener的定义如下,它可以在整个Retry前、整个Retry后和每次Retry失败时进行一些操作。

public interface RetryListener {

  /**
   * 在第一次尝试之前调用。如果方法的返回值是false,则不会进行尝试,反而会抛出TerminatedRetryException。
   *
   * @param <E> RetryCallback可抛出的异常类型
   * @param <T> RetryCallback的返回值类型
   * @param context 当前RetryContext.
   * @param callback 当前RetryCallback.
   * @return 如果需要继续尝试则返回true.
   */
  <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback);

  /**
   * 在最后一次尝试后调用,而不管最后一次尝试是成功的还是失败的。
   *
   * @param context 当前RetryContext.
   * @param callback 当前RetryCallback.
   * @param throwable RetryCallback抛出的最后一个异常.
   * @param <E> RetryCallback可抛出的异常类型
   * @param <T> RetryCallback的返回值类型
   */
  <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable);

  /**
   * 每一次尝试失败都会调用一次
   *
   * @param context 当前RetryContext.
   * @param callback 当前RetryCallback.
   * @param throwable RetryCallback抛出的最后一个异常.
   * @param <E> RetryCallback可抛出的异常类型
   * @param <T> RetryCallback的返回值类型
   */
  <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable);
}

下面是一个简单的使用RetryListener的示例。

@Test
public void testListener() {
  RetryTemplate retryTemplate = new RetryTemplate();
  AtomicInteger counter = new AtomicInteger();
  RetryCallback<Integer, IllegalStateException> retryCallback = retryContext -> {
    //内部默认重试策略是最多尝试3次,即最多重试两次。还不成功就会抛出异常。
    if (counter.incrementAndGet() < 3) {
      throw new IllegalStateException();
    }
    return counter.incrementAndGet();
  };

  RetryListener retryListener = new RetryListener() {
    @Override
    public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
      System.out.println("---open----在第一次重试时调用");
      return true;
    }

    @Override
    public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
      System.out.println("close----在最后一次重试后调用(无论成功与失败)。" + context.getRetryCount());
    }

    @Override
    public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
      System.out.println("error----在每次调用异常时调用。" + context.getRetryCount());
    }
  };

  retryTemplate.registerListener(retryListener);
  retryTemplate.execute(retryCallback);

}

如果只想关注RetryListener的某些方法,则可以选择继承RetryListenerSupport,它默认实现了RetryListener的所有方法。

 

声明式的重试(使用注解)

Spring Retry支持对Spring bean使用声明式的重试,在需要重试的bean方法上加上@Retryable。使用这种机制需要在@Configuration类上加上@EnableRetry

@EnableRetry
@Configuration
public class RetryConfiguration {

  @Bean
  public HelloService helloService() {
    return new HelloService();
  }

}

这样就启用了声明式的重试机制,其会对使用了@Retryable标注的方法对应的bean创建对应的代理。使用@Retryable标注的方法如果不特殊声明的话,默认最多可以尝试3次。

public class HelloService {

  @Retryable
  public void hello(AtomicInteger counter) {
    if (counter.incrementAndGet() < 10) {
      throw new IllegalStateException();
    }
  }

}

@Retryable也可以加在Class上,当加在Class上时表示该bean所有的对外方法都是可以重试的。当Class上和方法上都加了@Retryable时,方法上的优先级更高。默认的最大尝试次数是3次,可以通过maxAttempts属性进行自定义。默认会对所有的异常进行重试,如有需要可以通过value和include属性指定需要重试的异常,也可以通过exclude属性指定不需要进行重试的异常。可以通过backoff属性指定BackOffPolicy相关的信息,它对应一个@BackOff,默认使用的BackOffPolicy将每次都间隔1000毫秒,如果默认值不能满足要求可以通过@BackOff指定初始的间隔时间。可以通过@BackOff的multiplier属性指定间隔之间的倍数,默认是0,即每次都是固定的间隔时间。当指定了multiplier后可以通过maxDelay属性指定最大的间隔时间,默认是0,表示不限制,即取ExponentialBackOffPolicy的默认值30秒。

@Retryable
public class HelloService {

  @Retryable(maxAttempts = 5, backoff = @Backoff(delay = 100, maxDelay = 2000, multiplier = 2))
  public void hello(AtomicInteger counter) {
    if (counter.incrementAndGet() < 10) {
      throw new IllegalStateException();
    }
  }

}

上面这些参数都是直接在代码里面写死的,如果你想改你还得修改代码,这很麻烦,所以对于这种可能会进行修改的参数我们一般会配置在配置文件中。上面这些属性都有增加Expression后缀的属性,比如maxAttempts对应的是maxAttemptsExpression,它是字符串类型,在里面可以使用占位符,从而允许我们在配置文件中配置这些信息。

@Retryable
public class HelloService {

  @Retryable(maxAttemptsExpression = "${retry.maxAttempts:5}",
          backoff = @Backoff(delayExpression = "${retry.delay:100}",
                  maxDelayExpression = "${retry.maxDelay:2000}",
                  multiplierExpression = "${retry.multiplier:2}"))
  public void hello(AtomicInteger counter) {
    if (counter.incrementAndGet() < 10) {
      throw new IllegalStateException();
    }
  }

}

 

Recover

使用注解的可重试方法,如果重试次数达到后还是继续失败的就会抛出异常,它可以通过@Recover标记同一Class中的一个方法作为RecoveryCallback。@Recover标记的方法的返回类型必须与@Retryable标记的方法一样。方法参数可以与@Retryable标记的方法一致,也可以不带参数,带了参数就会传递过来。

@Retryable
public class HelloService {

  @Retryable(maxAttemptsExpression = "${retry.maxAttempts:5}",
          backoff = @Backoff(delayExpression = "${retry.delay:100}",
                  maxDelayExpression = "${retry.maxDelay:2000}",
                  multiplierExpression = "${retry.multiplier:2}"))
  public void hello(AtomicInteger counter) {
    if (counter.incrementAndGet() < 10) {
      throw new IllegalStateException();
    }
  }

  @Recover
  public void helloRecover(AtomicInteger counter) {
    counter.set(1000);
  }

}

@Recover标记的方法还可以选择包含一个Exception类型的参数,它对应于@Retryable标记的方法最后抛出的异常,如果需要包含异常参数该参数必须是第一个参数。当定义了多个@Recover方法时,Spring Retry将选择更精确的那一个。此时的RecoveryCallback将选择第二个helloRecover方法。

@Retryable
public class HelloService {

  @Retryable(maxAttemptsExpression = "${retry.maxAttempts:5}",
          backoff = @Backoff(delayExpression = "${retry.delay:100}",
                  maxDelayExpression = "${retry.maxDelay:2000}",
                  multiplierExpression = "${retry.multiplier:2}"))
  public void hello(AtomicInteger counter) {
    if (counter.incrementAndGet() < 10) {
      throw new IllegalStateException();
    }
  }

  @Recover
  public void helloRecover(AtomicInteger counter) {
    counter.set(1000);
  }

  @Recover
  public void helloRecover(IllegalStateException e, AtomicInteger counter) {
    counter.set(2000);
  }

}

 

监听器

使用声明式的Spring Retry,如果需要使用RetryListener,只需把它们定义为一个Spring bean即可。比如下面这样。

@EnableRetry
@Configuration
@PropertySource("classpath:/application.properties")
public class RetryConfiguration {

  @Bean
  public HelloService helloService() {
    return new HelloService();
  }

  @Bean
  public RetryListener retryListener() {
    return new RetryListenerSupport() {
      @Override
      public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
        super.onError(context, callback, throwable);
        System.out.println("发生异常:" + context.getRetryCount());
      }
    };
  }

}

(注:本文是基于Spring Retry1.2.2所写)

0
0
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics