Fork me on GitHub

SpringRetry框架简介

前言

今天我们来聊一聊Spring Retry框架。

Spring Retry提供了一个关于重试失败操作的抽象,强调对流程和基于策略的行为的声明性控制,易于扩展和定制。例如,对于一个操作,如果它失败了,我们可以根据异常的类型,使用一个固定的或指数级的回退来重试它。

并不是所有的异常失败都适合重试,比如参数校验错误,显然不适合重试,而Spring Retry可以指定要重试的异常类型,对于指定类型的异常进行重试。

考虑到网络原因,可能一些方法失败后不立即进行下一次重试,而等待若干时间后再进行,Spring Retry里也支持此种类型的重试。

可能所有的重试都不成功,此时需要返回一个程序默认值或者直接抛出异常等,Spring Retry的兜底函数可以解决此类问题。

另外Spring Retry还支持简单的熔断策略。

正文

说了这么多,我们来看下Spring Retry吧。

要使用Spring Retry,首先要引入相关jar包,如下:

1
2
3
4
5
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>1.2.2.RELEASE</version>
</dependency>

先简单的写一个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class Test {
public static void main(String[] args) {
RetryTemplate template = new RetryTemplate();
//重试策略:次数重试策略
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
retryPolicy.setMaxAttempts(3);
template.setRetryPolicy(retryPolicy);
//退避策略:固定退避策略
FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
backOffPolicy.setBackOffPeriod(4*1000L);
template.setBackOffPolicy(backOffPolicy);

String str = "";
//当重试失败后,执行RecoveryCallback
String result2 = template.execute((retryCallback)->{
System.out.println("retry count:" + retryCallback.getRetryCount());
return getStr(str);
}, (recoveryCallback)-> {
System.out.println("所有重试均失败!!");
return "";
});
System.out.println("返回值为:"+result2);
}

/**
* 测试方法
* @param str
* @return
*/
public static String getStr(String str){
if(StringUtils.isBlank(str)){
throw new RuntimeException("数据为空!");
}
return str;
}
}

上面的代码,当我们传入空或者空字符串时,可以看到程序会重试3次(每隔4s),均不成功,最后返回recoveryCallback的数据。

1
2
3
4
5
6
7
8
9
10
11
12
18:05:20.879 [main] DEBUG org.springframework.retry.support.RetryTemplate - Retry: count=0
retry count:0
18:05:24.889 [main] DEBUG org.springframework.retry.support.RetryTemplate - Checking for rethrow: count=1
18:05:24.889 [main] DEBUG org.springframework.retry.support.RetryTemplate - Retry: count=1
retry count:1
18:05:28.890 [main] DEBUG org.springframework.retry.support.RetryTemplate - Checking for rethrow: count=2
18:05:28.890 [main] DEBUG org.springframework.retry.support.RetryTemplate - Retry: count=2
retry count:2
18:05:28.890 [main] DEBUG org.springframework.retry.support.RetryTemplate - Checking for rethrow: count=3
18:05:28.890 [main] DEBUG org.springframework.retry.support.RetryTemplate - Retry failed last attempt: count=3
所有重试均失败!!
返回值为:失败

我们来看下例子中涉及到的一些东西。

可以看到,要使用重试功能,首先要创建一个RetryTemplate,并设置它的两个重要参数:重试策略(RetryPolicy)和退避策略(BackOffPolicy)。

重试策略

这两个策略还是比较好理解的,对于重试策略,指的就是请求不成功后下次请求的策略。很明显我们可以看到它是一个接口RetryPolicy。

这个接口里比较重要的一个方法为canRetry,它的返回值决定下一次是否重试。

1
boolean canRetry(RetryContext context);

对于这个接口,可以看到它目前有8种重试策略。

upload successful

  • NeverRetryPolicy

    只调用被执行方法一次,不会进行重试操作。

    我们可以看到它的canRetry方法。可以看到这个方法会一直返回false。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
       public boolean canRetry(RetryContext context) {
    return !((NeverRetryContext) context).isFinished();
    }
    private static class NeverRetryContext extends RetryContextSupport {
    private boolean finished = false;

    public NeverRetryContext(RetryContext parent) {
    super(parent);
    }

    public boolean isFinished() {
    return finished;
    }

    public void setFinished() {
    this.finished = true;
    }
    }
  • AlwaysRetryPolicy

    如果被执行方法调用不成功会一直重试,这种方法如果操作不当会出现死循环的情况,应当注意。

    我们可以看到它里面的canRetry方法一直返回true,即如果调用失败,会一直重试直到成功。

    1
    2
    3
       public boolean canRetry(RetryContext context) {
    return true;
    }
  • SimpleRetryPolicy

    固定次数重试策略,默认最多重试3次,我们可以通过指定其maxAttempts参数的值来规定最多重试多少次。

    它的canRetry方法可以看到和当前已重试次数做了比较来确定下一次是否重试。

    1
    2
    3
    4
       public boolean canRetry(RetryContext context) {
    Throwable t = context.getLastThrowable();
    return (t == null || retryForException(t)) && context.getRetryCount() < maxAttempts;
    }
  • TimeoutRetryPolicy

    超时重试策略,只有在超时时间内才可以重试,超过后就不会再进行重试,超时时间可以认为是在第一次请求开始时计数。默认超时时间1000ms,我们可以通过设置timeout的值来指定超时时间。

    它的canRetry方法,可以看到时间的对比来确定是否进行重试。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
       public boolean canRetry(RetryContext context) {
    return ((TimeoutRetryContext) context).isAlive();
    }
    private static class TimeoutRetryContext extends RetryContextSupport {
    private long timeout;

    private long start;

    public TimeoutRetryContext(RetryContext parent, long timeout) {
    super(parent);
    this.start = System.currentTimeMillis();
    this.timeout = timeout;
    }

    public boolean isAlive() {
    return (System.currentTimeMillis() - start) <= timeout;
    }
    }
  • CompositeRetryPolicy

    组合重试策略,有乐观重试和悲观重试两种情况。可以看到它有两个参数,optimistic和policies。

    optimistic表示是否乐观,默认false。

    policies表示所有传入的重试策略。

    我们根据它的canRetry方法,可以清楚的知道,如果乐观情况下,有一个策略(policies[i])canRetry为true就可以进行重试,悲观情况下只有所有的传入的重试策略canRetry为true才可以进行重试。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
       public boolean canRetry(RetryContext context) {
    RetryContext[] contexts = ((CompositeRetryContext) context).contexts;
    RetryPolicy[] policies = ((CompositeRetryContext) context).policies;

    boolean retryable = true;

    if(this.optimistic) {
    retryable = false;
    for (int i = 0; i < contexts.length; i++) {
    if (policies[i].canRetry(contexts[i])) {
    retryable = true;
    }
    }
    }
    else {
    for (int i = 0; i < contexts.length; i++) {
    if (!policies[i].canRetry(contexts[i])) {
    retryable = false;
    }
    }
    }

    return retryable;
    }
  • ExpressionRetryPolicy

    异常重试策略,会对抛出指定异常的情况下进行重试,继承SimpleRetryPolicy。可以指定要重试的异常参数expression,也可以指定异常的全名字符串,会被转化为指定异常。

    我们看一下它的canRetry方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
       public boolean canRetry(RetryContext context) {
    Throwable lastThrowable = context.getLastThrowable();
    if (lastThrowable == null) {
    return super.canRetry(context);
    }
    else {
    return super.canRetry(context)
    && this.expression.getValue(this.evaluationContext, lastThrowable, Boolean.class);
    }
    }

    可以看到除了使用了SimpleRetryPolicy的canRetry判断还有对是不是当前异常的判断,来确定是否重试。

    当然这个策略也是可以指定最大重试次数maxAttempts的。

  • ExceptionClassifierRetryPolicy

    根据最新的异常动态的适应注入的策略,需要设置参数exceptionClassifier。

    比如第一次重试时,抛出异常A,对应传入策略A,当第二次重试时,抛出异常B,则对应传入的策略B。

    1
    2
    3
    4
       public boolean canRetry(RetryContext context) {
    RetryPolicy policy = (RetryPolicy) context;
    return policy.canRetry(context);
    }

    可以看到它的canRetry返回值取决于当前使用的策略的canRetry方法的返回值,而策略的动态切换由ExceptionClassifierRetryContext这个类来处理,这儿不再过多介绍。

  • CircuitBreakerRetryPolicy

    带有熔断的重试策略,该策略提供过载保护功能,它的canRetry代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
       public boolean canRetry(RetryContext context) {
    CircuitBreakerRetryContext circuit = (CircuitBreakerRetryContext) context;
    //如果熔断器处于打开状态,就直接短路,返回失败
    if (circuit.isOpen()) {
    circuit.incrementShortCircuitCount();
    return false;
    }
    else {
    //重置熔断器
    circuit.reset();
    }
    return this.delegate.canRetry(circuit.context);
    }
    //------- isOpen方法如下
    public boolean isOpen() {
    long time = System.currentTimeMillis() - this.start;
    boolean retryable = this.policy.canRetry(this.context);
    //当前不允许重试
    if (!retryable) {
    //如果已经超过重置时间,重新闭合,关闭熔断器
    if (time > this.timeout) {
    logger.trace("Closing");
    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) {
    logger.trace("Opening circuit");
    setAttribute(CIRCUIT_OPEN, true);
    }
    this.start = System.currentTimeMillis();
    return true;
    }
    }
    //允许重试
    else {
    //判断是否在openWindow熔断器电路打开的超时时间之外,超过打开时间,就重置上下文,并且返回false
    if (time > this.openWindow) {
    logger.trace("Resetting context");
    this.start = System.currentTimeMillis();
    this.context = createDelegateContext(policy, getParent());
    }
    }
    if (logger.isTraceEnabled()) {
    logger.trace("Open: " + !retryable);
    }
    setAttribute(CIRCUIT_OPEN, !retryable);
    return !retryable;
    }

    它接受三个参数,delegate、resetTimeout和openTimeout。

    delegate指使用的重试策略,默认使用SimpleRetryPolicy。

    resetTimeout表示重置线路超时时间(以毫秒为单位)。当线路打开后,它会在此时间过后重新关闭,上下文将重新启动。

    openTimeout表示断开线路的超时时间。如果委托策略无法重试,则自上下文启动以来经过的时间小于此时间,则打开线路。

退避策略

我们再来看一下退避策略(BackOffPolicy)。

退避策略接口(BackOffPolicy)目前有5种已实现策略。如下图:

upload successful

我们来分别看一下它们。

要实现退避策略,重要的是实现接口的backoff方法。

1
2
3
4
public interface BackOffPolicy {
BackOffContext start(RetryContext context);
void backOff(BackOffContext backOffContext) throws BackOffInterruptedException;
}

这个方法的实现有两个主要类,抽象类StatelessBackOffPolicy和实现类ExponentialBackOffPolicy。

如图:

upload successful

  • StatelessBackOffPolicy

    这是用于在调用之间不维护任何状态的退避策略实现的简单基类,它的backoff方法调用了子类的doBackOff方法。

    1
    2
    3
    public final void backOff(BackOffContext backOffContext) throws BackOffInterruptedException {
    doBackOff();
    }

    可以看到它的三个实现并简单分析,如下图。

    upload successful

    • NoBackOffPolicy

      无任何退避策略,可以看到doBackOff方法什么也没做。

      1
      2
         protected void doBackOff() throws BackOffInterruptedException {
      }

      这种情况下,如果一次重试不成功,下一次会直接再进行重试。

    • FixedBackOffPolicy

      固定退避策略,这种情况下,一次重试不成功,下一次会间隔一段时间后在进行重试。

      可以看到它可以通过设置backOffPeriod(退避间隔)来指定与下一次重试的间隔时间。这个值默认为1000ms。

      这个类里面另一个比较重要的参数为Sleeper(休眠器),它可以指定程序的休眠方式,默认使用ThreadWaitSleeper休眠器。

      可以看到它的doBackOff方法直接调用了休眠器的sleep方法休眠一段时间。

      1
      2
      3
      4
      5
      6
      7
      8
         protected void doBackOff() throws BackOffInterruptedException {
      try {
      sleeper.sleep(backOffPeriod);
      }
      catch (InterruptedException e) {
      throw new BackOffInterruptedException("Thread interrupted while sleeping", e);
      }
      }
    • UniformRandomBackOffPolicy

      随机休眠退避策略,当一次重试失败后,下一次重试之前,这个策略会随机退避一段时间。

      看到这个我们明显就知道它会有minBackOffPeriod(最小退避时间)和maxBackOffPeriod(最大退避时间)两个值了。最小退避值默认500ms,最大退避值默认1500ms。

      除了上面两个参数,它里面比较重要的两个参数一个是取值器和休眠器。

      1
      2
         private Random random = new Random(System.currentTimeMillis());
      private Sleeper sleeper = new ThreadWaitSleeper();

      上面代码可以看到它们的值(random取值器不可人为修改)。

      再来看下doBackOff方法。

      1
      2
      3
      4
      5
      6
      7
      8
      9
         protected void doBackOff() throws BackOffInterruptedException {
      try {
      long delta = maxBackOffPeriod==minBackOffPeriod ? 0 : random.nextInt((int) (maxBackOffPeriod - minBackOffPeriod));
      sleeper.sleep(minBackOffPeriod + delta );
      }
      catch (InterruptedException e) {
      throw new BackOffInterruptedException("Thread interrupted while sleeping", e);
      }
      }

      也是比较好理解的,可以看到当最大时间和最小时间相等时,delta=0,即每次重试之前都休眠minBackOffPeriod时间。

  • ExponentialBackOffPolicy

    指数型退避策略,顾名思义,它的退避时间是指数增长的。

    我们来看下它的三个参数,initialInterval 初始时间间隔,maxInterval 最大时间间隔,multiplier指数因子。

    来看一下它的backOff方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
       public void backOff(BackOffContext backOffContext)
    throws BackOffInterruptedException {
    ExponentialBackOffContext context = (ExponentialBackOffContext) backOffContext;
    try {
    long sleepTime = context.getSleepAndIncrement();
    if (logger.isDebugEnabled()) {
    logger.debug("Sleeping for " + sleepTime);
    }
    sleeper.sleep(sleepTime);
    }
    catch (InterruptedException e) {
    throw new BackOffInterruptedException("Thread interrupted while sleeping", e);
    }
    }

    以及它涉及到的下面的方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
       public synchronized long getSleepAndIncrement() {
    long sleep = this.interval;
    if (sleep > maxInterval) {
    sleep = maxInterval;
    }
    else {
    this.interval = getNextInterval();
    }
    return sleep;
    }

    protected long getNextInterval() {
    return (long) (this.interval * this.multiplier);
    }

    可以看到逻辑很好理解,默认退避时间为interval,如果interval超过maxInterval,退避时间就为maxInterval,否则就获取下一次的interval时间,这个时间就是interval*multiplier,所以退避时间会以指数增长。

    它的另一个参数Sleeper(休眠器)默认也是ThreadWaitSleeper。

    initialInterval初始时间默认值为100ms,maxInterval最大时间默认为30000ms,multiplier指数因子默认为2.

  • ExponentialRandomBackOffPolicy

    随机指数退避策略,对于上面的指数策略,这儿不一样的就是指数因子会随机变化。

    我们大致看一下这个策略的源码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
     public class ExponentialRandomBackOffPolicy extends ExponentialBackOffPolicy {
    //部分代码略
    @Override
    static class ExponentialRandomBackOffContext
    extends ExponentialBackOffPolicy.ExponentialBackOffContext {
    private final Random r = new Random();

    public ExponentialRandomBackOffContext(long expSeed, double multiplier,
    long maxInterval) {
    super(expSeed, multiplier, maxInterval);
    }

    @Override
    public synchronized long getSleepAndIncrement() {
    long next = super.getSleepAndIncrement();
    next = (long) (next * (1 + r.nextFloat() * (getMultiplier() - 1)));
    return next;
    }

    }
    //部分代码略
    }

    可以看到它继承了ExponentialBackOffPolicy,并重写了ExponentialBackOffContext里的getSleepAndIncrement方法,原来的指数因子改为随机的了。

    其它与ExponentialBackOffPolicy一致,这儿不再介绍。

RetryTemplate

再来看下重试模板RetryTemplate,除了上面说到RetryPolicy和BackOffPolicy,它还有几个比较重要的参数。

  • RetryListener :可以传入一个listener数组,主要功能是用于监控重试行为。
  • RetryCallback :重试回调,用户包装业务流,第一次执行和产生重试执行都会调用这个callback代码。
  • RecoveryCallback :当所有重试都失败后,回调该接口,提供给业务重试回复机制。
  • RetryState :重试状态,对于一些有事务的方法,如果出现某些异常,可能需要回滚而不是进行重试,这个参数可以完成这一功能。
  • RetryContext : 重试上下文,每次重试都会将其作为参数传入RetryCallback中使用。

然后我们大致来看下RetryTemplate的部分关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
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);
RetrySynchronizationManager.register(context);
Throwable lastException = null;
boolean exhausted = false;
try {
//执行切面方法,在执行业务之前可以通过listener进行监控
boolean running = doOpenInterceptors(retryCallback, context);
if (!running) {
throw new TerminatedRetryException(
"Retry terminated abnormally by interceptor before first attempt");
}
//确定退避上下文环境
BackOffContext backOffContext = null;
Object resource = context.getAttribute("backOffContext");
if (resource instanceof BackOffContext) {
backOffContext = (BackOffContext) resource;
}
if (backOffContext == null) {
backOffContext = backOffPolicy.start(context);
if (backOffContext != null) {
context.setAttribute("backOffContext", backOffContext);
}
}
//开始重试循环
//如果重试策略认为可以重试
while (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) {
try {
//清空上次的异常
lastException = null;
//执行业务方法
return retryCallback.doWithRetry(context);
}
catch (Throwable e) {
//出现异常,最新异常就是此次异常
lastException = e;
try {
//将异常信息通知到retryPolicy、state和context
registerThrowable(retryPolicy, state, context, e);
}
catch (Exception ex) {
throw new TerminatedRetryException("Could not register throwable",
ex);
}
finally {
//对于重试出现的异常,我们使用切面listener进行监听
doOnErrorInterceptors(retryCallback, context, e);
}
//如果重试策略认为还可以重试
if (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) {
try {
//执行退避策略
backOffPolicy.backOff(backOffContext);
}
catch (BackOffInterruptedException ex) {
lastException = e;
throw ex;
}
}
//确认下是否需要重新抛出(对于有事务的逻辑,重新抛出指定异常方便事务回滚)
if (shouldRethrow(retryPolicy, context, state)) {
throw RetryTemplate.<E>wrapIfNecessary(e);
}

}
if (state != null && context.hasAttribute(GLOBAL_STATE)) {
break;
}
}
//重试完成后,执行recoveryCallback操作
exhausted = true;
return handleRetryExhausted(recoveryCallback, context, state);

}
catch (Throwable e) {
throw RetryTemplate.<E>wrapIfNecessary(e);
}
finally {
//关闭
close(retryPolicy, context, state, lastException == null || exhausted);
//使用切面listener进行监控关闭等流程
doCloseInterceptors(retryCallback, context, lastException);
RetrySynchronizationManager.clear();
}

}

上述代码中用到的一些方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
   //该异常是否抛出
protected boolean shouldRethrow(RetryPolicy retryPolicy, RetryContext context,
RetryState state) {
return state != null && state.rollbackFor(context.getLastThrowable());
}
//监听open操作
private <T, E extends Throwable> boolean doOpenInterceptors(
RetryCallback<T, E> callback, RetryContext context) {

boolean result = true;

for (RetryListener listener : this.listeners) {
result = result && listener.open(context, callback);
}

return result;

}
//监听close操作
private <T, E extends Throwable> void doCloseInterceptors(
RetryCallback<T, E> callback, RetryContext context, Throwable lastException) {
for (int i = this.listeners.length; i-- > 0;) {
this.listeners[i].close(context, callback, lastException);
}
}
//监听error操作
private <T, E extends Throwable> void doOnErrorInterceptors(
RetryCallback<T, E> callback, RetryContext context, Throwable throwable) {
for (int i = this.listeners.length; i-- > 0;) {
this.listeners[i].onError(context, callback, throwable);
}
}

根据上面的描述,RetryTemplate的执行流程大致如下:

upload successful

其它

我们把开始提到的例子复杂化下。引入Listener和RetryState参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class Test {
public static void main(String[] args) {
RetryTemplate template = new RetryTemplate();
//重试策略:次数重试策略
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
retryPolicy.setMaxAttempts(3);
template.setRetryPolicy(retryPolicy);
//退避策略:固定退避策略
FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
backOffPolicy.setBackOffPeriod(1000L);
template.setBackOffPolicy(backOffPolicy);
//设置有状态重试
BinaryExceptionClassifier classifier = new BinaryExceptionClassifier(
Collections.singleton(NullPointerException.class)
);
RetryState state = new DefaultRetryState("rollbackKey", false, classifier);
//设置监听
DefaultStatisticsRepository defaultStatisticsRepository =new DefaultStatisticsRepository();
template.setListeners(new RetryListener[]{new StatisticsListener(defaultStatisticsRepository)});

String str = "1";
//当重试失败后,执行RecoveryCallback
String result2 = template.execute((retryCallback)->{
System.out.println("retry count:" + retryCallback.getRetryCount());
retryCallback.setAttribute(RetryContext.NAME,"method.key");
return getStr(str);
}, (recoveryCallback)-> {
System.out.println("所有重试均失败!!");
return "失败";
},state);
RetryStatistics statistics = defaultStatisticsRepository.findOne("method.key");
System.out.println(statistics);

}
public static String getStr(String str){
if(StringUtils.isBlank(str)){
throw new NullPointerException("数据为空!");
}
if("1".equals(str)){
throw new RuntimeException("参数为1!!!");
}
return str;
}
}

我们对str赋值1和””,可以清楚的看到输出的日志。当赋值””时,执行一次后直接抛出空指针异常,不会再进行重试。如果调用的方法有事务,可以进行回滚等操作,这就是有状态的重试。

当str=”1”时,可以看到监听分析的结果:

1
DefaultRetryStatistics [name=method.key, startedCount=0, completeCount=0, recoveryCount=1, errorCount=3, abortCount=0]

重试注解

Spring Retry也支持使用注解的形式标注。如下:

EnableRetry

1
2
3
4
5
6
7
8
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@EnableAspectJAutoProxy(proxyTargetClass = false)
@Import(RetryConfiguration.class)
@Documented
public @interface EnableRetry {
boolean proxyTargetClass() default false;
}
proxyTargetClass指是否使用CGLIB增强代理,默认false。

这个注解作用在类上,如果想要某个方法可以进行重试,则这个方法所在的类需要有EnableRetry注解。

Retryable

内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Retryable {
String interceptor() default "";
Class<? extends Throwable>[] value() default {};
Class<? extends Throwable>[] include() default {};
Class<? extends Throwable>[] exclude() default {};
String label() default "";
boolean stateful() default false;
int maxAttempts() default 3;
String maxAttemptsExpression() default "";
Backoff backoff() default @Backoff();
String exceptionExpression() default "";
}

该注解作用在方法上,指定的方法会进行重试操作。

参数说明:

interceptor:拦截器
value:可以重试的异常类型,如果为空并且exclude为空,则会重试所有异常,与include同义。
include:与value同义。
exclude:不需要重试的异常。
label:分析报告的名称,listener相关使用。
stateful:是否有状态重试,有的话指定的异常要抛出而不是重试。
maxAttempts:最大重试次数。
maxAttemptsExpression:最大重试次数表达式。
backoff:退避策略,详见BackOff注解。
exceptionExpression:异常表达式,要抛出的异常(有状态情况下)的表达式。

Backoff

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(RetryConfiguration.class)
@Documented
public @interface Backoff {
long value() default 1000;
long delay() default 0;
long maxDelay() default 0;
double multiplier() default 0;
String delayExpression() default "";
String maxDelayExpression() default "";
String multiplierExpression() default "";
boolean random() default false;
}

退避策略注解,使用方式见上面Retryable的backoff值。

主要参数说明:

value:退避间隔,和delay同义。
delay:与value同义。在随机退避策略里表示最小值,在指数退避策略和随机指数退避策略里表示起始值。
maxDelay:在随机退避策略里表示最大值,在指数退避策略和随机指数退避策略里表示最大值。
multiplier:指数因子。
delayExpression:退避间隔表达式。
maxDelayExpression:最大值表达式。
multiplierExpression:指数因子表达式。
random:是否随机。

可以看到,如果什么也不设置,将使用NoBackOffPolicy。如果只设置value或者delay值,将使用FixedBackOffPolicy。如果还设置了maxDelay和random,将使用UniformRandomBackOffPolicy……

CircuitBreaker

熔断注解。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Retryable(stateful = true)
public @interface CircuitBreaker {
Class<? extends Throwable>[] value() default {};
Class<? extends Throwable>[] include() default {};
Class<? extends Throwable>[] exclude() default {};
int maxAttempts() default 3;
String label() default "";
long resetTimeout() default 20000;
long openTimeout() default 5000;
}

主要参数与上面说的Retryable基本说明一样,它的其它两个参数resetTimeout和openTimeout上面已经讲过。

Recover

1
2
3
4
5
6
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Import(RetryConfiguration.class)
@Documented
public @interface Recover {
}

这个注解也作用于方法上,表示所有重试失败后兜底的返回信息,这个作用的方法,应该有以下特性:

  • 第一个参数是重试的程序抛出的异常(需要重试的异常)。
  • 后面的参数应该与Retryable注释的入参一致,返回值也应一致。
  • 第一个参数可选,但是如果不写,需要保证Retryable在没有其他的Recover匹配的情况下才会被调用。

我们使用注解来简单写个例子,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Service
@Slf4j
@EnableRetry
public class RetryTest {
@Transactional
@Retryable(value = Exception.class,maxAttempts = 5,backoff = @Backoff(2000L))
public String retryTest(String str1,Integer integer1){
if(StringUtils.isBlank(str1)){
throw new NullPointerException("str1参数为空!!");
}
if(integer1==null){
throw new RuntimeException("integer1参数不正确!!!");
}
return str1+integer1;
}
@Recover
public String recover(Exception e,String str1,Integer integer1){
log.info("所有重试均失败,返回兜底值",e);
return "";
}
}

可以看到我们创建了一个重试方法,这个方法最多重试5次,每重试一次之前都会退避2s后再进行,重试所有异常,当所有重试均不成功后会返回兜底值””。

总结

通过对Spring Retry框架的理解,我们对重试框架有了一个更全面的认识,了解了它的一些简单实现原理,明白了它的一些关键参数。如果有方法有重试需求,可以适当进行Spring Retry框架的考虑。




-------------文章结束啦 ~\(≧▽≦)/~ 感谢您的阅读-------------

SakuraTears wechat
扫一扫关注我的公众号
您的支持就是我创作的动力!
0%