前言
今天我们来聊一聊Spring Retry框架。
Spring Retry提供了一个关于重试失败操作的抽象,强调对流程和基于策略的行为的声明性控制,易于扩展和定制。例如,对于一个操作,如果它失败了,我们可以根据异常的类型,使用一个固定的或指数级的回退来重试它。
并不是所有的异常失败都适合重试,比如参数校验错误,显然不适合重试,而Spring Retry可以指定要重试的异常类型,对于指定类型的异常进行重试。
考虑到网络原因,可能一些方法失败后不立即进行下一次重试,而等待若干时间后再进行,Spring Retry里也支持此种类型的重试。
可能所有的重试都不成功,此时需要返回一个程序默认值或者直接抛出异常等,Spring Retry的兜底函数可以解决此类问题。
另外Spring Retry还支持简单的熔断策略。
正文
说了这么多,我们来看下Spring Retry吧。
要使用Spring Retry,首先要引入相关jar包,如下:
1 | <dependency> |
先简单的写一个例子。
1 | public class Test { |
上面的代码,当我们传入空或者空字符串时,可以看到程序会重试3次(每隔4s),均不成功,最后返回recoveryCallback的数据。
1 | 18:05:20.879 [main] DEBUG org.springframework.retry.support.RetryTemplate - Retry: count=0 |
我们来看下例子中涉及到的一些东西。
可以看到,要使用重试功能,首先要创建一个RetryTemplate,并设置它的两个重要参数:重试策略(RetryPolicy)和退避策略(BackOffPolicy)。
重试策略
这两个策略还是比较好理解的,对于重试策略,指的就是请求不成功后下次请求的策略。很明显我们可以看到它是一个接口RetryPolicy。
这个接口里比较重要的一个方法为canRetry,它的返回值决定下一次是否重试。
1 | boolean canRetry(RetryContext context); |
对于这个接口,可以看到它目前有8种重试策略。
NeverRetryPolicy
只调用被执行方法一次,不会进行重试操作。
我们可以看到它的canRetry方法。可以看到这个方法会一直返回false。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public 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
3public boolean canRetry(RetryContext context) {
return true;
}SimpleRetryPolicy
固定次数重试策略,默认最多重试3次,我们可以通过指定其maxAttempts参数的值来规定最多重试多少次。
它的canRetry方法可以看到和当前已重试次数做了比较来确定下一次是否重试。
1
2
3
4public 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
18public 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
24public 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
10public 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
4public 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
51public 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种已实现策略。如下图:
我们来分别看一下它们。
要实现退避策略,重要的是实现接口的backoff方法。
1 | public interface BackOffPolicy { |
这个方法的实现有两个主要类,抽象类StatelessBackOffPolicy和实现类ExponentialBackOffPolicy。
如图:
StatelessBackOffPolicy
这是用于在调用之间不维护任何状态的退避策略实现的简单基类,它的backoff方法调用了子类的doBackOff方法。
1
2
3public final void backOff(BackOffContext backOffContext) throws BackOffInterruptedException {
doBackOff();
}可以看到它的三个实现并简单分析,如下图。
NoBackOffPolicy
无任何退避策略,可以看到doBackOff方法什么也没做。
1
2protected void doBackOff() throws BackOffInterruptedException {
}这种情况下,如果一次重试不成功,下一次会直接再进行重试。
FixedBackOffPolicy
固定退避策略,这种情况下,一次重试不成功,下一次会间隔一段时间后在进行重试。
可以看到它可以通过设置backOffPeriod(退避间隔)来指定与下一次重试的间隔时间。这个值默认为1000ms。
这个类里面另一个比较重要的参数为Sleeper(休眠器),它可以指定程序的休眠方式,默认使用ThreadWaitSleeper休眠器。
可以看到它的doBackOff方法直接调用了休眠器的sleep方法休眠一段时间。
1
2
3
4
5
6
7
8protected 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
2private Random random = new Random(System.currentTimeMillis());
private Sleeper sleeper = new ThreadWaitSleeper();上面代码可以看到它们的值(random取值器不可人为修改)。
再来看下doBackOff方法。
1
2
3
4
5
6
7
8
9protected 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
14public 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
14public 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
22public class ExponentialRandomBackOffPolicy extends ExponentialBackOffPolicy {
//部分代码略
static class ExponentialRandomBackOffContext
extends ExponentialBackOffPolicy.ExponentialBackOffContext {
private final Random r = new Random();
public ExponentialRandomBackOffContext(long expSeed, double multiplier,
long maxInterval) {
super(expSeed, multiplier, maxInterval);
}
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 | protected <T, E extends Throwable> T doExecute(RetryCallback<T, E> retryCallback, |
上述代码中用到的一些方法如下:
1 | //该异常是否抛出 |
根据上面的描述,RetryTemplate的执行流程大致如下:
其它
我们把开始提到的例子复杂化下。引入Listener和RetryState参数。
1 | public class Test { |
我们对str赋值1和””,可以清楚的看到输出的日志。当赋值””时,执行一次后直接抛出空指针异常,不会再进行重试。如果调用的方法有事务,可以进行回滚等操作,这就是有状态的重试。
当str=”1”时,可以看到监听分析的结果:
1 | DefaultRetryStatistics [name=method.key, startedCount=0, completeCount=0, recoveryCount=1, errorCount=3, abortCount=0] |
重试注解
Spring Retry也支持使用注解的形式标注。如下:
EnableRetry
1 |
|
proxyTargetClass指是否使用CGLIB增强代理,默认false。
这个注解作用在类上,如果想要某个方法可以进行重试,则这个方法所在的类需要有EnableRetry注解。
Retryable
内容如下:
1 |
|
该注解作用在方法上,指定的方法会进行重试操作。
参数说明:
interceptor:拦截器
value:可以重试的异常类型,如果为空并且exclude为空,则会重试所有异常,与include同义。
include:与value同义。
exclude:不需要重试的异常。
label:分析报告的名称,listener相关使用。
stateful:是否有状态重试,有的话指定的异常要抛出而不是重试。
maxAttempts:最大重试次数。
maxAttemptsExpression:最大重试次数表达式。
backoff:退避策略,详见BackOff注解。
exceptionExpression:异常表达式,要抛出的异常(有状态情况下)的表达式。
Backoff
1 |
|
退避策略注解,使用方式见上面Retryable的backoff值。
主要参数说明:
value:退避间隔,和delay同义。
delay:与value同义。在随机退避策略里表示最小值,在指数退避策略和随机指数退避策略里表示起始值。
maxDelay:在随机退避策略里表示最大值,在指数退避策略和随机指数退避策略里表示最大值。
multiplier:指数因子。
delayExpression:退避间隔表达式。
maxDelayExpression:最大值表达式。
multiplierExpression:指数因子表达式。
random:是否随机。
可以看到,如果什么也不设置,将使用NoBackOffPolicy。如果只设置value或者delay值,将使用FixedBackOffPolicy。如果还设置了maxDelay和random,将使用UniformRandomBackOffPolicy……
CircuitBreaker
熔断注解。
1 |
|
主要参数与上面说的Retryable基本说明一样,它的其它两个参数resetTimeout和openTimeout上面已经讲过。
Recover
1 |
|
这个注解也作用于方法上,表示所有重试失败后兜底的返回信息,这个作用的方法,应该有以下特性:
- 第一个参数是重试的程序抛出的异常(需要重试的异常)。
- 后面的参数应该与Retryable注释的入参一致,返回值也应一致。
- 第一个参数可选,但是如果不写,需要保证Retryable在没有其他的Recover匹配的情况下才会被调用。
我们使用注解来简单写个例子,如下:
1 |
|
可以看到我们创建了一个重试方法,这个方法最多重试5次,每重试一次之前都会退避2s后再进行,重试所有异常,当所有重试均不成功后会返回兜底值””。
总结
通过对Spring Retry框架的理解,我们对重试框架有了一个更全面的认识,了解了它的一些简单实现原理,明白了它的一些关键参数。如果有方法有重试需求,可以适当进行Spring Retry框架的考虑。