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 RetryCallbackextends Throwable> {
T doWithRetry(RetryContext context) throws E;
}
当RetryCallback不能再重试的时候,如果定义了RecoveryCallback,就会调用RecoveryCallback,并以其返回结果作为execute()
的返回结果。其定义如下。RetryCallback和RecoverCallback定义的接口方法都可以接收一个RetryContext参数,通过它可以获取到尝试次数,也可以通过其setAttribute()
和getAttribute()
来传递一些信息。
public interface RecoveryCallback {
T recover(RetryContext context) throws Exception;
}
Spring Retry包括有状态的重试和无状态的重试,对于有状态的重试,它主要用来提供一个用于在RetryContextCache中保存RetryContext的Key,这样可以在多次不同的调用中应用同一个RetryContext(无状态的重试每次发起调用都是一个全新的RetryContext,在整个重试过程中是一个RetryContext,其不会进行保存。有状态的重试因为RetryContext是保存的,其可以跨或不跨线程在多次execute()
调用中应用同一个RetryContext)。
使用Spring Retry需要引入如下依赖。
<dependency>
<groupId>org.springframework.retrygroupId>
<artifactId>spring-retryartifactId>
<version>1.2.4.RELEASEversion>
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 IllegalArgumentException();
}
return counter.get();
});
}
CircuitBreakerRetryPolicy
CircuitBreakerRetryPolicy是包含了断路器功能的RetryPolicy,它内部默认包含了一个SimpleRetryPolicy,最多尝试3次。在固定的时间窗口内(默认是20秒)如果底层包含的RetryPolicy的尝试次数都已经耗尽了,则其会打开断路器,默认打开时间是5秒,在这段时间内如果还有其它请求过来就不会再进行调用了。CircuitBreakerRetryPolicy需要跟RetryState一起使用,下面的代码中RetryTemplate使用的是CircuitBreakerRetryPolicy,一共调用了5次execute()
,每次调用RetryCallback都会抛出IllegalStateException,并且会打印counter的当前值,前三次RetryCallback都是可以运行的,之后断路器打开了,第四五次执行execute()
时就不会再执行RetryCallback了,所以你只能看到只进行了3次打印。
@Test
public void testCircuitBreakerRetryPolicy() throws Exception {
CircuitBreakerRetryPolicy retryPolicy = new CircuitBreakerRetryPolicy();
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setRetryPolicy(retryPolicy);
AtomicInteger counter = new AtomicInteger();
RetryState retryState = new DefaultRetryState("key");
for (int i=0; i<5; i++) {
try {
retryTemplate.execute(retryContext -> {
System.out.println(LocalDateTime.now() + "----" + counter.get());
TimeUnit.MILLISECONDS.sleep(100);
if (counter.incrementAndGet() > 0) {
throw new IllegalStateException();
}
return 1;
}, null, retryState);
} catch (Exception e) {
}
}
}
断路器默认打开的时间是5秒,5秒之后断路器又会关闭,RetryCallback又可以正常调用了。判断断路器是否需要打开的时间窗口默认是20秒,即在20秒内所有的尝试次数都用完了,就会打开断路器。如果在20秒内只尝试了两次(默认3次),则在新的时间窗口内尝试次数又将从0开始计算。可以通过如下方式进行这两个时间的设置。
SimpleRetryPolicy delegate = new SimpleRetryPolicy(5);
//底层允许最多尝试5次
CircuitBreakerRetryPolicy retryPolicy = new CircuitBreakerRetryPolicy(delegate);
retryPolicy.setOpenTimeout(2000);//断路器打开的时间
retryPolicy.setResetTimeout(15000);//时间窗口
CompositeRetryPolicy
CompositeRetryPolicy可以用来组合多个RetryPolicy,可以设置必须所有的RetryPolicy都是可以重试的时候才能进行重试,也可以设置只要有一个RetryPolicy可以重试就可以进行重试。默认是必须所有的RetryPolicy都可以重试才能进行重试。下面代码中应用的就是CompositeRetryPolicy,它组合了两个RetryPolicy,最多尝试5次的SimpleRetryPolicy和超时时间是2秒钟的TimeoutRetryPolicy,所以它们的组合就是必须尝试次数不超过5次且尝试时间不超过2秒钟才能进行重试。execute()
中执行的RetryCallback的逻辑是counter的值小于10时就抛出IllegalStateException,否则就返回counter的值。第一次尝试的时候会失败,第二次也是,直到第5次尝试也还是失败的,此时SimpleRetryPolicy已经不能再尝试了,而TimeoutRetryPolicy此时还是可以尝试的,但是由于前者已经不能再尝试了,所以整体就不能再尝试了。所以下面的执行会以抛出IllegalStateException告终。
@Test
public void testCompositeRetryPolicy() {
CompositeRetryPolicy compositeRetryPolicy = new CompositeRetryPolicy();
RetryPolicy policy1 = new SimpleRetryPolicy(5);
TimeoutRetryPolicy policy2 = new TimeoutRetryPolicy();
policy2.setTimeout(2000);
RetryPolicy[] policies = new RetryPolicy[]{policy1, policy2};
compositeRetryPolicy.setPolicies(policies);
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setRetryPolicy(compositeRetryPolicy);
AtomicInteger counter = new AtomicInteger();
retryTemplate.execute(retryContext -> {
if (counter.incrementAndGet() < 10) {
throw new IllegalStateException();
}
return counter.get();
});
}
CompositeRetryPolicy也支持组合的RetryPolicy中只要有一个RetryPolicy满足条件就可以进行重试,这是通过参数optimistic控制的,默认是false,改为true即可。比如下面设置了setOptimistic(true)
,那么中尝试5次后SimpleRetryPolicy已经不满足了,但是TimeoutRetryPolicy还满足条件,所以最终会一直尝试,直到counter的值为10。
@Test
public void testCompositeRetryPolicy() {
CompositeRetryPolicy compositeRetryPolicy = new CompositeRetryPolicy();
RetryPolicy policy1 = new SimpleRetryPolicy(5);
TimeoutRetryPolicy policy2 = new TimeoutRetryPolicy();
policy2.setTimeout(2000);
RetryPolicy[] policies = new RetryPolicy[]{policy1, policy2};
compositeRetryPolicy.setPolicies(policies);
compositeRetryPolicy.setOptimistic(true);
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setRetryPolicy(compositeRetryPolicy);
AtomicInteger counter = new AtomicInteger();
Integer result = retryTemplate.execute(retryContext -> {
if (counter.incrementAndGet() < 10) {
throw new IllegalStateException();
}
return counter.get();
});
Assert.assertEquals(10, result.intValue());
}
BackOffPolicy
BackOffPolicy用来定义在两次尝试之间需要间隔的时间,RetryTemplate内部默认使用的是NoBackOffPolicy,其在两次尝试之间不会进行任何的停顿。对于一般可重试的操作往往是基于网络进行的远程请求,它可能由于网络波动暂时不可用,如果立马进行重试它可能还是不可用,但是停顿一下,过一会再试可能它又恢复正常了,所以在RetryTemplate中使用BackOffPolicy往往是很有必要的。
FixedBackOffPolicy
FixedBackOffPolicy将在两次重试之间进行一次固定的时间间隔,默认是1秒钟,也可以通过setBackOffPeriod()
进行设置。下面代码中指定了两次重试的时间间隔是1秒钟,第一次尝试会失败,等一秒后会进行第二次尝试,第二次尝试会成功。
@Test
public void testFixedBackOffPolicy() {
FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
backOffPolicy.setBackOffPeriod(1000);
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setBackOffPolicy(backOffPolicy);
long t1 = System.currentTimeMillis();
long t2 = retryTemplate.execute(retryContext -> {
if (System.currentTimeMillis() - t1 < 1000) {
throw new IllegalStateException();
}
return System.currentTimeMillis();
});
Assert.assertTrue(t2 - t1 > 1000);
Assert.assertTrue(t2 - t1 < 1100);
}
ExponentialBackOffPolicy
ExponentialBackOffPolicy可以使每一次尝试的间隔时间都不一样,它有3个重要的参数,初始间隔时间、后一次间隔时间相对于前一次间隔时间的倍数和最大的间隔时间,它们的默认值分别是100毫秒、2.0和30秒。下面的代码使用了ExponentialBackOffPolicy,指定了初始间隔时间是1000毫秒,每次间隔时间以2倍的速率递增,最大的间隔时间是5000毫秒,它最多可以尝试10次。所以当第1次尝试失败后会间隔1秒后进行第2次尝试,之后再间隔2秒进行第3次尝试,之后再间隔4秒进行第4次尝试,之后都是间隔5秒再进行下一次尝试,因为再翻倍已经超过了设定的最大的间隔时间。
@Test
public void testExponentialBackOffPolicy() {
ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
backOffPolicy.setInitialInterval(1000);
backOffPolicy.setMaxInterval(5000);
backOffPolicy.setMultiplier(2.0);
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setBackOffPolicy(backOffPolicy);
int maxAttempts = 10;
retryTemplate.setRetryPolicy(new SimpleRetryPolicy(maxAttempts));
long t1 = System.currentTimeMillis();
long t2 = retryTemplate.execute(retryContext -> {
if (retryContext.getRetryCount() < maxAttempts-1) {//最后一次尝试会成功
throw new IllegalStateException();
}
return System.currentTimeMillis();
});
long time = 0 + 1000 + 1000 * 2 + 1000 * 2 * 2 + 5000 * (maxAttempts - 4);
Assert.assertTrue((t2-t1) - time < 100);
}
ExponentialRandomBackOffPolicy
ExponentialRandomBackOffPolicy的用法跟ExponentialBackOffPolicy的用法是一样的,它继承自ExponentialBackOffPolicy,在确定间隔时间时会先按照ExponentialBackOffPolicy的方式确定一个时间间隔,然后再随机的增加一个0-1的。比如取得的随机数是0.1即表示增加10%,每次需要确定重试间隔时间时都会产生一个新的随机数。如果指定的初始间隔时间是100毫秒,增量倍数是2,最大间隔时间是2000毫秒,则按照ExponentialBackOffPolicy的重试间隔是100、200、400、800,而ExponentialRandomBackOffPolicy产生的间隔时间可能是111、256、421、980。下面代码使用了ExponentialRandomBackOffPolicy,它打印出了每次重试时间的间隔。如果你有兴趣,你运行它会看到它会在ExponentialBackOffPolicy的基础上每次都随机的增长0-1倍。
@Test
public void testExponentialRandomBackOffPolicy() {
ExponentialRandomBackOffPolicy backOffPolicy = new ExponentialRandomBackOffPolicy();
backOffPolicy.setInitialInterval(1000);
backOffPolicy.setMaxInterval(5000);
backOffPolicy.setMultiplier(2.0);
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setBackOffPolicy(backOffPolicy);
int maxAttempts = 10;
retryTemplate.setRetryPolicy(new SimpleRetryPolicy(maxAttempts));
String lastAttemptTime = "lastAttemptTime";
retryTemplate.execute(retryContext -> {
if (retryContext.hasAttribute(lastAttemptTime)) {
System.out.println(System.currentTimeMillis() - (Long) retryContext.getAttribute(lastAttemptTime));
}
retryContext.setAttribute(lastAttemptTime, System.currentTimeMillis());
if (retryContext.getRetryCount() < maxAttempts-1) {//最后一次尝试会成功
throw new IllegalStateException();
}
return System.currentTimeMillis();
});
}
UniformRandomBackOffPolicy
UniformRandomBackOffPolicy用来每次都随机的产生一个间隔时间,默认的间隔时间是在500-1500毫秒之间。可以通过setMinBackOffPeriod()
设置最小间隔时间,通过setMaxBackOffPeriod()
设置最大间隔时间。
@Test
public void testUniformRandomBackOffPolicy() {
UniformRandomBackOffPolicy backOffPolicy = new UniformRandomBackOffPolicy();
backOffPolicy.setMinBackOffPeriod(1000);
backOffPolicy.setMaxBackOffPeriod(3000);
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setBackOffPolicy(backOffPolicy);
int maxAttempts = 10;
retryTemplate.setRetryPolicy(new SimpleRetryPolicy(maxAttempts));
String lastAttemptTime = "lastAttemptTime";
retryTemplate.execute(retryContext -> {
if (retryContext.hasAttribute(lastAttemptTime)) {
System.out.println(System.currentTimeMillis() - (Long) retryContext.getAttribute(lastAttemptTime));
}
retryContext.setAttribute(lastAttemptTime, System.currentTimeMillis());
if (retryContext.getRetryCount() < maxAttempts-1) {//最后一次尝试会成功
throw new IllegalStateException();
}
return System.currentTimeMillis();
});
}
监听器
RetryTemplate中可以注册一些RetryListener,它可以用来对整个Retry过程进行监听。RetryListener的定义如下,它可以在整个Retry前、整个Retry后和每次Retry失败时进行一些操作。
public interface RetryListener {
/**
* 在第一次尝试之前调用。如果方法的返回值是false,则不会进行尝试,反而会抛出TerminatedRetryException。
*
* @param RetryCallback可抛出的异常类型
* @param RetryCallback的返回值类型
* @param context 当前RetryContext.
* @param callback 当前RetryCallback.
* @return 如果需要继续尝试则返回true.
*/
<T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback);
/**
* 在最后一次尝试后调用,而不管最后一次尝试是成功的还是失败的。
*
* @param context 当前RetryContext.
* @param callback 当前RetryCallback.
* @param throwable RetryCallback抛出的最后一个异常.
* @param RetryCallback可抛出的异常类型
* @param RetryCallback的返回值类型
*/
<T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable);
/**
* 每一次尝试失败都会调用一次
*
* @param context 当前RetryContext.
* @param callback 当前RetryCallback.
* @param throwable RetryCallback抛出的最后一个异常.
* @param RetryCallback可抛出的异常类型
* @param RetryCallback的返回值类型
*/
<T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable);
}
下面是一个简单的使用RetryListener的示例。
@Test
public void testListener() {
RetryTemplate retryTemplate = new RetryTemplate();
AtomicInteger counter = new AtomicInteger();
RetryCallback<Integer, IllegalStateException> retryCallback = retryContext -> {
//内部默认重试策略是最多尝试3次,即最多重试两次。还不成功就会抛出异常。
if (counter.incrementAndGet() < 3) {
throw new IllegalStateException();
}
return counter.incrementAndGet();
};
RetryListener retryListener = new RetryListener() {
@Override
public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
System.out.println("---open----在第一次重试时调用");
return true;
}
@Override
public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
System.out.println("close----在最后一次重试后调用(无论成功与失败)。" + context.getRetryCount());
}
@Override
public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
System.out.println("error----在每次调用异常时调用。" + context.getRetryCount());
}
};
retryTemplate.registerListener(retryListener);
retryTemplate.execute(retryCallback);
}
如果只想关注RetryListener的某些方法,则可以选择继承RetryListenerSupport,它默认实现了RetryListener的所有方法。
声明式的重试(使用注解)
Spring Retry支持对Spring bean使用声明式的重试,在需要重试的bean方法上加上@Retryable
。使用这种机制需要在@Configuration
类上加上@EnableRetry
。
@EnableRetry
@Configuration
public class RetryConfiguration {
@Bean
public HelloService helloService() {
return new HelloService();
}
}
这样就启用了声明式的重试机制,其会对使用了@Retryable
标注的方法对应的bean创建对应的代理。使用@Retryable
标注的方法如果不特殊声明的话,默认最多可以尝试3次。
public class HelloService {
@Retryable
public void hello(AtomicInteger counter) {
if (counter.incrementAndGet() < 10) {
throw new IllegalStateException();
}
}
}
@Retryable
也可以加在Class上,当加在Class上时表示该bean所有的对外方法都是可以重试的。当Class上和方法上都加了@Retryable
时,方法上的优先级更高。默认的最大尝试次数是3次,可以通过maxAttempts属性进行自定义。默认会对所有的异常进行重试,如有需要可以通过value和include属性指定需要重试的异常,也可以通过exclude属性指定不需要进行重试的异常。可以通过backoff属性指定BackOffPolicy相关的信息,它对应一个@BackOff
,默认使用的BackOffPolicy将每次都间隔1000毫秒,如果默认值不能满足要求可以通过@BackOff
指定初始的间隔时间。可以通过@BackOff
的multiplier属性指定间隔之间的倍数,默认是0,即每次都是固定的间隔时间。当指定了multiplier后可以通过maxDelay属性指定最大的间隔时间,默认是0,表示不限制,即取ExponentialBackOffPolicy的默认值30秒。
@Retryable
public class HelloService {
@Retryable(maxAttempts = 5, backoff = @Backoff(delay = 100, maxDelay = 2000, multiplier = 2))
public void hello(AtomicInteger counter) {
if (counter.incrementAndGet() < 10) {
throw new IllegalStateException();
}
}
}
上面这些参数都是直接在代码里面写死的,如果你想改你还得修改代码,这很麻烦,所以对于这种可能会进行修改的参数我们一般会配置在配置文件中。上面这些属性都有增加Expression后缀的属性,比如maxAttempts对应的是maxAttemptsExpression,它是字符串类型,在里面可以使用占位符,从而允许我们在配置文件中配置这些信息。
@Retryable
public class HelloService {
@Retryable(maxAttemptsExpression = "${retry.maxAttempts:5}",
backoff = @Backoff(delayExpression = "${retry.delay:100}",
maxDelayExpression = "${retry.maxDelay:2000}",
multiplierExpression = "${retry.multiplier:2}"))
public void hello(AtomicInteger counter) {
if (counter.incrementAndGet() < 10) {
throw new IllegalStateException();
}
}
}
Recover
使用注解的可重试方法,如果重试次数达到后还是继续失败的就会抛出异常,它可以通过@Recover
标记同一Class中的一个方法作为RecoveryCallback。@Recover
标记的方法的返回类型必须与@Retryable
标记的方法一样。方法参数可以与@Retryable
标记的方法一致,也可以不带参数,带了参数就会传递过来。
@Retryable
public class HelloService {
@Retryable(maxAttemptsExpression = "${retry.maxAttempts:5}",
backoff = @Backoff(delayExpression = "${retry.delay:100}",
maxDelayExpression = "${retry.maxDelay:2000}",
multiplierExpression = "${retry.multiplier:2}"))
public void hello(AtomicInteger counter) {
if (counter.incrementAndGet() < 10) {
throw new IllegalStateException();
}
}
@Recover
public void helloRecover(AtomicInteger counter) {
counter.set(1000);
}
}
@Recover
标记的方法还可以选择包含一个Exception类型的参数,它对应于@Retryable
标记的方法最后抛出的异常,如果需要包含异常参数该参数必须是第一个参数。当定义了多个@Recover
方法时,Spring Retry将选择更精确的那一个。此时的RecoveryCallback将选择第二个helloRecover方法。
@Retryable
public class HelloService {
@Retryable(maxAttemptsExpression = "${retry.maxAttempts:5}",
backoff = @Backoff(delayExpression = "${retry.delay:100}",
maxDelayExpression = "${retry.maxDelay:2000}",
multiplierExpression = "${retry.multiplier:2}"))
public void hello(AtomicInteger counter) {
if (counter.incrementAndGet() < 10) {
throw new IllegalStateException();
}
}
@Recover
public void helloRecover(AtomicInteger counter) {
counter.set(1000);
}
@Recover
public void helloRecover(IllegalStateException e, AtomicInteger counter) {
counter.set(2000);
}
}
监听器
使用声明式的Spring Retry,如果需要使用RetryListener,只需把它们定义为一个Spring bean即可。比如下面这样。
@EnableRetry
@Configuration
@PropertySource("classpath:/application.properties")
public class RetryConfiguration {
@Bean
public HelloService helloService() {
return new HelloService();
}
@Bean
public RetryListener retryListener() {
return new RetryListenerSupport() {
@Override
public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
super.onError(context, callback, throwable);
System.out.println("发生异常:" + context.getRetryCount());
}
};
}
}
(注:本文是基于Spring Retry1.2.2所写)