Bootstrap

spring-retry

写在前面

本篇文章仅作为近日参考其他文章后,自己实践的记录和总结,场景到细节尚有很多不足,有待补充和修正。

概述

spring-retry是spring提供的一套请求重试机制组件。对方法调用时产生的各种Exception捕捉并按照配置的重试策略进行重试有较好的支持。

重试策略(RetryPolicy)

在正式开始之前,我们可以先了解一下spring-retry的重试策略。

package org.springframework.retry;

import java.io.Serializable;

public interface RetryPolicy extends Serializable {
    //判断是否可以触发重试机制
    boolean canRetry(RetryContext var1);

    RetryContext open(RetryContext var1);

    void close(RetryContext var1);
    //设置那些异常可以触发重试机制
    void registerThrowable(RetryContext var1, Throwable var2);
}

由RetryPolicy接口类方法可见,重试策略将决定在什么场景下可以触发重试(是否可以触发重试机制)。

首先是spring-retry自带的集中实现类策略,如下:
在这里插入图片描述

下面表格简单介绍了几种重试策略,由于内容是copy过来,暂且记录,有待整理探解。

重试策略说明
SimpleRetryPolicy (org.springframework.retry.policy)固定次数重试策略,默认重试最大次数为3次,RetryTemplate默认使用的策略
CircuitBreakerRetryPolicy (org.springframework.retry.policy)有熔断功能的重试策略,需设置3个参数openTimeout、resetTimeout和delegate:
1.openTimeout 接口执行时间超时多久后开启熔断,单位ms
2.resetTimeout 熔断持续时间,单位ms
3.熔断代表着其他请求过来时将不会执行
ExceptionClassifierRetryPolicy (org.springframework.retry.policy)设置不同异常的重试策略,类似组合重试策略,区别在于这里只区分不同异常的重试
InterceptorRetryPolicy (org.springframework.cloud.client.loadbalancer)尚不理解
CompositeRetryPolicy (org.springframework.retry.policy)组合重试策略,有两种组合方式,乐观组合重试策略是指只要有一个策略允许即可以重试,悲观组合重试策略是指只要有一个策略不允许即可以重试,但不管哪种组合方式,组合中的每一个策略都会执行
NeverRetryPolicy (org.springframework.retry.policy)只允许调用RetryCallback一次,不允许重试
TimeoutRetryPolicy (org.springframework.retry.policy)超时时间重试策略,默认超时时间为1秒,在指定的超时时间内允许重试

退避策略(BackOffPolicy)

当RetryPolicy判断出场景触发重试之后,退避策略将决定等待多久后开始重试。

package org.springframework.retry.backoff;

import org.springframework.retry.RetryContext;

public interface BackOffPolicy {

	/**
	 * Start a new block of back off operations. Implementations can choose to
	 * pause when this method is called, but normally it returns immediately.
	 * 
	 * 实现的策略可以在这个方法去提供一个初始化的参数,可以联系到上下文,也可以自定义参数来供backOff()使用
	 * 
	 * @param context the {@link RetryContext} context, which might contain information
	 * that we can use to decide how to proceed.
	 * @return the implementation-specific {@link BackOffContext} or '<code>null</code>'.
	 */
    BackOffContext start(RetryContext var1);

	/**
	 * Back off/pause in an implementation-specific fashion. The passed in
	 * {@link BackOffContext} corresponds to the one created by the call to
	 * {@link #start} for a given retry operation set.
	 * 
	 * 根据start方法提供的BackOffContext来执行,在这个方法中执行sleep阻塞
	 * 
	 * @throws BackOffInterruptedException if the attempt at back off is
	 * interrupted.
	 * @param backOffContext the {@link BackOffContext}
	 */
    void backOff(BackOffContext var1) throws BackOffInterruptedException;
}

下面是自带的实现类
在这里插入图片描述

从上面实现树可见,实现类主要分为两大类:

SleepingBackOffPolicy代表等待的实现方式依赖线程的休眠(Thread.sleep),接口类需要主要添加了withSleepe方法,将sleeper加入到等待策略中,除了NoBackOffPolicy策略不等待立即重试外,其他提供的策略都实现了SleepingBackOffPolicy

package org.springframework.retry.backoff;

/**
 * A interface which can be mixed in by {@link BackOffPolicy}s indicating that they sleep
 * when backing off.
 */
public interface SleepingBackOffPolicy<T extends SleepingBackOffPolicy<T>> extends BackOffPolicy {
    /**
     * Clone the policy and return a new policy which uses the passed sleeper.
     *
     * @param sleeper  Target to be invoked any time the backoff policy sleeps
     * @return a clone of this policy which will have all of its backoff sleeps
     *         routed into the passed sleeper
     */
    T withSleeper(Sleeper sleeper);
}

StatelessBackOffPolicy表意为无状态方式,根据下面所示其实现类可知,start直接返回null,即不需要额外的上下文参数,目前只有ExponentialBackOffPolicy和ExponentialRandomBackOffPolicy不是无状态退避策略,因为指数退避策略在start方法需要额外提供BackOffContext,我想ExponentialRandomBackOffPolicy的实现方式也可以帮助我们了解如何建立自己的退避策略。

/**
 * Simple base class for {@link BackOffPolicy} implementations that maintain no
 * state across invocations.
 * 
 * @author Rob Harrop
 * @author Dave Syer
 */
public abstract class StatelessBackOffPolicy implements BackOffPolicy {

	/**
	 * Delegates directly to the {@link #doBackOff()} method without passing on
	 * the {@link BackOffContext} argument which is not needed for stateless
	 * implementations.
	 */
	public final void backOff(BackOffContext backOffContext) throws BackOffInterruptedException {
		doBackOff();
	}

	/**
	 * Returns '<code>null</code>'. Subclasses can add behaviour, e.g.
	 * initial sleep before first attempt.
	 */
	public BackOffContext start(RetryContext status) {
		return null;
	}

	/**
	 * Sub-classes should implement this method to perform the actual back off.
	 */
	protected abstract void doBackOff() throws BackOffInterruptedException;
}
回退策略说明
ExponentialBackOffPolicy (org.springframework.retry.backoff)指数退避策略,需设置参数sleeper、initialInterval、maxInterval和multiplier,initialInterval指定初始休眠时间,默认100毫秒,maxInterval指定最大休眠时间,默认30秒,multiplier指定乘数,即下一次休眠时间为当前休眠时间*multiplier
ExponentialRandomBackOffPolicy (org.springframework.retry.backoff)在 ExponentialBackOffPolicy 的策略基础上,在获取指针策略的等待时间后,再随机乘以一个随机数作为实际sleep时间
UniformRandomBackOffPolicy (org.springframework.retry.backoff)随机时间退避策略,需设置sleeper、minBackOffPeriod和maxBackOffPeriod,该策略在[minBackOffPeriod,maxBackOffPeriod之间取一个随机休眠时间,minBackOffPeriod默认500毫秒,maxBackOffPeriod默认1500毫秒
NoBackOffPolicy (org.springframework.retry.backoff)无退避,重试策略判定需要重试后立马重试
FixedBackOffPolicy (org.springframework.retry.backoff)固定时间的退避策略,需设置参数sleeper和backOffPeriod,sleeper指定等待策略,默认是Thread.sleep,即线程休眠,backOffPeriod指定休眠时间,默认1秒

简单的使用(SimpleRetryPolicy+FixedBackOffPolicy)

实际上SimpleRetryPolicy和NoBackOffPolicy,这里使用FixedBackOffPolicy做示例

首先引入依赖

<dependency>
   <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>
<!--使用RetryTemplate 可能会需要这个-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>4.3.12.RELEASE</version>
</dependency>

自定义RetryTemplate

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.retry.backoff.FixedBackOffPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;

@Configuration
public class TestBeanProvider {

    @Bean("simpleRetryTemplate")
    public RetryTemplate simpleRetryTemplate(){
        RetryTemplate retryTemplate = new RetryTemplate();
        SimpleRetryPolicy simpleRetryPolicy = new SimpleRetryPolicy();
        //默认3次,设置为5次
        simpleRetryPolicy.setMaxAttempts(5);
        retryTemplate.setRetryPolicy(simpleRetryPolicy);
        //固定等待时间 10s
        FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
        fixedBackOffPolicy.setBackOffPeriod(10000);
        retryTemplate.setBackOffPolicy(fixedBackOffPolicy);
        return retryTemplate;
    }
}

使用RetryTemplate

@Autowired
@Qualifier("simpleRetryTemplate")
private  RetryTemplate  retryTemplate;

@GetMapping("/springRetryTest")
public void springRetryTest(){
    retryTemplate.execute(context ->{
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd HH:mm:ss");
        log.info("这是第"+(context.getRetryCount()+1)+"次重试:"+sdf.format(new Date()));
        throw new RuntimeException();
    });
}

下面是是执行日志

2022-06-15 20:35:27.265  INFO 7984 --- [nio-8081-exec-1] s.a.t.controller.TestController          : 这是第1次重试:20220615 20:35:27
2022-06-15 20:35:37.271  INFO 7984 --- [nio-8081-exec-1] s.a.t.controller.TestController          : 这是第2次重试:20220615 20:35:37
2022-06-15 20:35:47.282  INFO 7984 --- [nio-8081-exec-1] s.a.t.controller.TestController          : 这是第3次重试:20220615 20:35:47
2022-06-15 20:35:57.292  INFO 7984 --- [nio-8081-exec-1] s.a.t.controller.TestController          : 这是第4次重试:20220615 20:35:57
2022-06-15 20:36:07.307  INFO 7984 --- [nio-8081-exec-1] s.a.t.controller.TestController          : 这是第5次重试:20220615 20:36:07

更多使用有待补充

@Retryable的使用

相对于直接的使用retryTemplate来实现重试,使用@Retryable会更加方便,下面是@Retryable注解类

/**
 * Annotation for a method invocation that is retryable.
 *
 * @author Dave Syer
 * @author Artem Bilan
 * @author Gary Russell
 * @since 1.1
 *
 */
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Retryable {

	/**
	 * Retry interceptor bean name to be applied for retryable method. Is mutually
	 * exclusive with other attributes.
	 * @return the retry interceptor bean name
	 */
	String interceptor() default "";

	/**
	 * Exception types that are retryable. Synonym for includes(). Defaults to empty (and
	 * if excludes is also empty all exceptions are retried).
	 * @return exception types to retry
	 */
	Class<? extends Throwable>[] value() default {};

	/**
	 * Exception types that are retryable. Defaults to empty (and if excludes is also
	 * empty all exceptions are retried).
	 * @return exception types to retry
	 */
	Class<? extends Throwable>[] include() default {};

	/**
	 * Exception types that are not retryable. Defaults to empty (and if includes is also
	 * empty all exceptions are retried).
	 * @return exception types to retry
	 */
	Class<? extends Throwable>[] exclude() default {};

	/**
	 * A unique label for statistics reporting. If not provided the caller may choose to
	 * ignore it, or provide a default.
	 *
	 * @return the label for the statistics
	 */
	String label() default "";

	/**
	 * Flag to say that the retry is stateful: i.e. exceptions are re-thrown, but the
	 * retry policy is applied with the same policy to subsequent invocations with the
	 * same arguments. If false then retryable exceptions are not re-thrown.
	 * @return true if retry is stateful, default false
	 */
	boolean stateful() default false;

	/**
	 * @return the maximum number of attempts (including the first failure), defaults to 3
	 */
	int maxAttempts() default 3;

	/**
	 * @return an expression evaluated to the maximum number of attempts (including the first failure), defaults to 3
	 * Overrides {@link #maxAttempts()}.
	 * @since 1.2
	 */
	String maxAttemptsExpression() default "";

	/**
	 * Specify the backoff properties for retrying this operation. The default is no
	 * backoff, but it can be a good idea to pause between attempts (even at the cost of
	 * blocking a thread).
	 * @return a backoff specification
	 */
	Backoff backoff() default @Backoff();

	/**
	 * Specify an expression to be evaluated after the {@code SimpleRetryPolicy.canRetry()}
	 * returns true - can be used to conditionally suppress the retry. Only invoked after
	 * an exception is thrown. The root object for the evaluation is the last {@code Throwable}.
	 * Other beans in the context can be referenced.
	 * For example:
	 * <pre class=code>
	 *  {@code "message.contains('you can retry this')"}.
	 * </pre>
	 * and
	 * <pre class=code>
	 *  {@code "@someBean.shouldRetry(#root)"}.
	 * </pre>
	 * @return the expression.
	 * @since 1.2
	 */
	String exceptionExpression() default "";

}

从中挑出一些重要的属性说明

属性名称说明
interceptor重试方法使用的重试拦截器bean名称,和其他的属性互斥(哪个优先待确认
value哪些异常可以触发重试 ,是include的同义词,复制将会应用到include,默认为空
include哪些异常可以触发重试 ,默认为空
exclude哪些异常将不会触发重试,默认为空,如果和include属性同时为空,则所有的异常都将会触发重试

疑问: 如果include和exclude 设置了同样的异常,那么该异常是否会触发重试呢?
答: 年轻人我劝你善良
stateful是否是无状态
maxAttempts重试策略之最大尝试次数,默认3次
maxAttemptsExpression字面意思是使用表达式来提供最大重试次数,默认3次
backoff指定退避策略,默认是 @Backoff ,即立即重试,相当于NoBackOffPolicy
exceptionExpression不懂

上面我们提到有@Backoff注解,在@Backoff注解可以定义退避策略,下面是注解类

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(RetryConfiguration.class)
@Documented
public @interface Backoff {

	/**
	 * Synonym for {@link #delay()}.
	 *
	 * @return the delay in milliseconds (default 1000)
	 */
	long value() default 1000;

	/**
	 * A canonical backoff period. Used as an initial value in the exponential case, and
	 * as a minimum value in the uniform case.
	 * @return the initial or canonical backoff period in milliseconds (default 1000)
	 */
	long delay() default 0;

	/**
	 * The maximimum wait (in milliseconds) between retries. If less than the
	 * {@link #delay()} then ignored.
	 *
	 * @return the maximum delay between retries (default 0 = ignored)
	 */
	long maxDelay() default 0;

	/**
	 * If positive, then used as a multiplier for generating the next delay for backoff.
	 *
	 * @return a multiplier to use to calculate the next backoff delay (default 0 =
	 * ignored)
	 */
	double multiplier() default 0;

	/**
	 * An expression evaluating to the canonical backoff period. Used as an initial value
	 * in the exponential case, and as a minimum value in the uniform case.
	 * Overrides {@link #delay()}.
	 * @return the initial or canonical backoff period in milliseconds.
	 * @since 1.2
	 */
	String delayExpression() default "";

	/**
	 * An expression evaluating to the maximimum wait (in milliseconds) between retries.
	 * If less than the {@link #delay()} then ignored.
	 * Overrides {@link #maxDelay()}
	 *
	 * @return the maximum delay between retries (default 0 = ignored)
	 * @since 1.2
	 */
	String maxDelayExpression() default "";

	/**
	 * Evaluates to a vaule used as a multiplier for generating the next delay for backoff.
	 * Overrides {@link #multiplier()}.
	 *
	 * @return a multiplier expression to use to calculate the next backoff delay (default 0 =
	 * ignored)
	 * @since 1.2
	 */
	String multiplierExpression() default "";

	/**
	 * In the exponential case ({@link #multiplier()} &gt; 0) set this to true to have the
	 * backoff delays randomized, so that the maximum delay is multiplier times the
	 * previous delay and the distribution is uniform between the two values.
	 *
	 * @return the flag to signal randomization is required (default false)
	 */
	boolean random() default false;

}

从中挑出一些重要的属性说明

属性名称说明
value延迟重试的时间,默认1000ms,是delay的同义词
delay延迟重试的时间,默认值为1000ms
maxDelay最大延时时间,默认0ms
multiplier延时增长指数,每次重试之后,延迟时间都将会增长delay*multiplier,相当于ExponentialBackOffPolicy
random是否添加随机参数,默认false,设置为true且multiplier也设置了之后,相当于ExponentialRandomBackOffPolicy

下面测试一些常见场景:

  1. 无任何参数

    @Retryable
    @GetMapping("/springRetryTest2")
    public void springRetryTest2(){
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd HH:mm:ss");
        log.info("执行重试:"+sdf.format(new Date()));
        throw new RuntimeException();
    }
    

    执行日志:

    : 执行重试:20220615 22:03:46
    : 执行重试:20220615 22:03:47
    : 执行重试:20220615 22:03:48
    

    从中我们可以看到使用@Retryable默认的重试次数是3次,重试间隔时间是1s

  2. include和exclude 设置一个异常是否触发

    @Retryable(include = {RuntimeException.class},exclude = {RuntimeException.class})
    @GetMapping("/springRetryTest2")
    public void springRetryTest2(){
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd HH:mm:ss");
        log.info("执行重试:"+sdf.format(new Date()));
        throw new RuntimeException();
    }
    

    执行日志:

    : 执行重试:20220615 22:08:19
    

    从中我们可以看出,当include为空时,默认所有异常都会触发重试,当exclude 为空时,将不会排除任何异常,
    当include和exclude 都不为空时,则会以 include异常集合 minus exclude异常集合作为最终的可触发重试异常,如果像本例一样将include和exclude 设置为一样,方法只会执行一次,即不会执行任何重试操作,这样做无疑与使用@Retryable的初衷相悖。

  3. 下级继承的异常类是否可以触发上级异常类

    @Retryable(include = {Exception.class})
    @GetMapping("/springRetryTest2")
    public void springRetryTest2(){
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd HH:mm:ss");
        log.info("执行重试:"+sdf.format(new Date()));
        throw new RuntimeException();
    }
    
    : 执行重试:20220615 22:23:21
    : 执行重试:20220615 22:23:22
    : 执行重试:20220615 22:23:23
    

    经验证可知,子类异常可以触发父类异常的重试捕捉,此外我顺便做了额外的尝试,和想象的一样,父类异常将不会触发子类异常的重试捕捉

  4. 设置重试间隔时间

    @Retryable(backoff =@Backoff(delay = 5000L))
    @GetMapping("/springRetryTest2")
    public void springRetryTest2(){
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd HH:mm:ss");
        log.info("执行重试:"+sdf.format(new Date()));
        throw new RuntimeException();
    }
    

    执行日志:

    : 执行重试:20220615 22:28:12
    : 执行重试:20220615 22:28:17
    : 执行重试:20220615 22:28:22
    

    根据执行日志可见,重试间隔变成了设置的5000ms

spring-retry+ribbon

待补充

;