目录
写在前面
本篇文章仅作为近日参考其他文章后,自己实践的记录和总结,场景到细节尚有很多不足,有待补充和修正。
概述
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()} > 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 |
下面测试一些常见场景:
-
无任何参数
@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
-
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的初衷相悖。 -
下级继承的异常类是否可以触发上级异常类
@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
经验证可知,子类异常可以触发父类异常的重试捕捉,此外我顺便做了额外的尝试,和想象的一样,父类异常将不会触发子类异常的重试捕捉
-
设置重试间隔时间
@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
待补充