Spring(34)——Spring Retry介绍

Posted elim168

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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 IllegalArgumentExceptionspring-retry实现方法请求重试

spring-retry失败重试

spring-retry失败重试

SpringBoot 消息重试框架 spring-retry 和 guava-retry 详解

Spring Retry vs Hystrix

Spring异常重试框架Spring Retry