本项目为 Spring 应用提供声明式重试支持。它被用于 Spring Batch、Spring Integration 及其他项目中。 同时也支持命令式重试,以便显式使用。
该版本将 spring-retry 修复为 2.0.13,适用于 java8 以及 spring4 或 spring5, 更多信息请参考官方 Spring 2.0.13 版本
本节简要介绍如何开始使用 Spring Retry。 其中包含一个声明式示例和一个命令式示例。
以下示例展示了如何以声明式风格使用 Spring Retry:
@Configuration
@EnableRetry
public class Application {
@Bean
public Service service() {
return new Service();
}
}
@Service
class Service {
@Retryable(RemoteAccessException.class)
public void service() {
// ... do something
}
@Recover
public void recover(RemoteAccessException e) {
// ... panic
}
}
此示例调用 service 方法,如果该方法因 RemoteAccessException 而失败,则进行重试(默认最多重试三次),如果仍然失败,则尝试调用 recover 方法。
@Retryable 注解属性中提供了多种选项,用于包含和排除异常类型、限制重试次数以及设置退避策略。
前面展示的使用 @Retryable 注解以声明式方式应用重试处理的方法,会对 AOP 类产生额外的运行时依赖。有关如何在项目中解决此依赖的详细信息,请参阅
'Java Configuration for Retry Proxies' 部分。
以下示例展示了如何以命令式风格使用 Spring Retry(自 1.3 版本起可用):
RetryTemplate template = RetryTemplate.builder()
.maxAttempts(3)
.fixedBackoff(1000)
.retryOn(RemoteAccessException.class)
.build();
template.execute(ctx -> {
// ... do something
});
对于 1.3 之前的版本, 请参阅 RetryTemplate 部分中的示例。
Spring Retry 需要 Java 1.7 和 Maven 3.0.5(或更高版本)。 要构建,请运行以下 Maven 命令:
$ mvn install
本节讨论 Spring Retry 的功能,并展示如何使用其 API。
RetryTemplate为了使处理更加健壮并减少失败的可能性,有时自动重试失败的操作会有所帮助,以防它在后续尝试中可能成功。容易受到此类处理的错误本质上是暂时性的。例如,由于网络故障而失败的远程 Web 服务或 RMI 服务调用,或数据库更新中的 DeadLockLoserException,可能在短暂等待后自行解决。为了自动化此类操作的重试,Spring Retry 提供了 RetryOperations 策略。RetryOperations 接口定义如下:
public interface RetryOperations {
<T> T execute(RetryCallback<T> retryCallback) throws Exception;
<T> T execute(RetryCallback<T> retryCallback, RecoveryCallback<T> recoveryCallback)
throws Exception;
<T> T execute(RetryCallback<T> retryCallback, RetryState retryState)
throws Exception, ExhaustedRetryException;
<T> T execute(RetryCallback<T> retryCallback, RecoveryCallback<T> recoveryCallback,
RetryState retryState) throws Exception;
}
基础回调是一个简单的接口,允许你插入一些需要重试的业务逻辑:
public interface RetryCallback<T> {
T doWithRetry(RetryContext context) throws Throwable;
}
回调会被尝试执行,如果失败(抛出 Exception),则会重试,直到成功或实现决定中止。RetryOperations 接口中有多个重载的 execute 方法,用于处理所有重试尝试耗尽后的各种恢复用例,以及处理重试状态,这使客户端和实现能够在调用之间存储信息(稍后会详细介绍)。
RetryOperations 最简单的通用实现是 RetryTemplate。以下示例展示了如何使用它:
RetryTemplate template = new RetryTemplate();
TimeoutRetryPolicy policy = new TimeoutRetryPolicy();
policy.setTimeout(30000L);
template.setRetryPolicy(policy);
Foo result = template.execute(new RetryCallback<Foo>() {
public Foo doWithRetry(RetryContext context) {
// Do stuff that might fail, e.g. webservice operation
return result;
}
});
在前面的示例中,我们执行一次 Web 服务调用并将结果返回给用户。 如果该调用失败,则会重试,直到达到超时时间。
从 1.3 版本开始,还提供了 RetryTemplate 的流式配置,如下所示:
RetryTemplate.builder()
.maxAttempts(10)
.exponentialBackoff(100, 2, 10000)
.retryOn(IOException.class)
.traversingCauses()
.build();
RetryTemplate.builder()
.fixedBackoff(10)
.withinMillis(3000)
.build();
RetryTemplate.builder()
.infiniteRetry()
.retryOn(IOException.class)
.uniformRandomBackoff(1000, 3000)
.build();
RetryContextRetryCallback 的方法参数是一个 RetryContext。许多回调会忽略该上下文。然而,如有必要,你可以将其用作属性包,以便在迭代期间存储数据。
如果在同一线程中存在正在进行的嵌套重试,则 RetryContext 会有一个父上下文。父上下文有时可用于存储需要在多次 execute 调用之间共享的数据。
RecoveryCallback当重试耗尽时,RetryOperations 可以将控制权传递给另一个回调:RecoveryCallback。要使用此功能,客户端可以将这些回调一起传递给同一个方法,如下例所示:
Foo foo = template.execute(new RetryCallback<Foo>() {
public Foo doWithRetry(RetryContext context) {
// business logic here
},
new RecoveryCallback<Foo>() {
Foo recover(RetryContext context) throws Exception {
// recover logic here
}
});
如果业务逻辑在模板决定中止之前未能成功,客户端有机会通过恢复回调执行一些替代处理。
在最简单的情况下,重试只是一个 while 循环:RetryTemplate 可以持续尝试,直到成功或失败。RetryContext 包含一些状态,用于决定是重试还是中止。然而,这个状态位于栈上,无需将其存储在任何全局位置。因此,我们称之为“无状态重试”。无状态重试与有状态重试的区别体现在 RetryPolicy 的实现中(RetryTemplate 可以处理两者)。在无状态重试中,回调始终在与重试失败时相同的线程中执行。
当失败导致事务性资源变为无效时,有一些特殊的考虑。这不适用于简单的远程调用,因为(通常)没有事务性资源,但它有时适用于数据库更新,尤其是在使用 Hibernate 时。在这种情况下,立即重新抛出导致失败的异常才有意义,这样事务可以回滚,我们可以开始一个新的(且有效的)事务。
在这些情况下,无状态重试不够好,因为重新抛出和回滚必然涉及离开 RetryOperations.execute() 方法,并可能丢失栈上的上下文。为了避免丢失上下文,我们必须引入一种存储策略,将其从栈上提升出来并(至少)放入堆存储中。为此,Spring Retry 提供了一种名为 RetryContextCache 的存储策略,你可以将其注入到 RetryTemplate 中。RetryContextCache 的默认实现是内存中的,使用一个简单的 Map。它具有严格强制实施的最大容量,以避免内存泄漏,但它没有任何高级缓存特性(例如生存时间)。如果你需要这些特性,应考虑注入一个具有这些特性的 Map。对于集群环境中多进程的高级用法,你可能还需要考虑用某种集群缓存来实现 RetryContextCache(不过,即使在集群环境中,这也可能有些过度)。
RetryOperations 的部分职责是在失败的操作以新的执行(通常包装在新事务中)返回时识别它们。为便于此,Spring Retry 提供了 RetryState 抽象。它与 RetryOperations 中的特殊 execute 方法配合使用。
失败的操作通过跨多次重试调用识别状态来识别。要识别状态,你可以提供一个 RetryState 对象,它负责返回标识该项的唯一键。该标识符用作 RetryContextCache 中的键。
警告: 在
RetryState返回的键中实现Object.equals()和Object.hashCode()时要非常小心。最好的建议是使用业务键来标识项。对于 JMS 消息,你可以使用消息 ID。
当重试耗尽时,你还可以选择以不同的方式处理失败的项,而不是调用 RetryCallback(现在推测它很可能失败)。与无状态情况一样,此选项由 RecoveryCallback 提供,你可以通过将其传递给 RetryOperations 的 execute 方法来提供。
是否重试的决定实际上委托给常规的 RetryPolicy,因此关于限制和超时的常见关注点可以注入到那里(参见 Additional Dependencies 部分)。
在 RetryTemplate 内部,execute 方法中决定重试还是失败由 RetryPolicy 决定,它同时也是 RetryContext 的工厂。RetryTemplate 负责使用当前策略创建 RetryContext,并在每次尝试时将其传递给 RetryCallback。回调失败后,RetryTemplate 必须调用 RetryPolicy,要求其更新状态(状态存储在 RetryContext 中)。然后它询问策略是否可以再进行一次尝试。如果无法再进行尝试(例如,因为已达到限制或检测到超时),策略还负责识别耗尽状态——但不负责处理异常。RetryTemplate 抛出原始异常,除非在有状态且无可用恢复的情况下。在这种情况下,它会抛出 RetryExhaustedException。你也可以在 RetryTemplate 中设置一个标志,使其无条件抛出回调(即用户代码)中的原始异常。
提示: 失败本质上要么可重试,要么不可重试——如果业务逻辑总是抛出相同的异常,重试它也无济于事。因此,你不应针对所有异常类型进行重试。相反,应尽量只关注那些你预期可重试的异常。更激进地重试通常不会对业务逻辑造成危害,但这样做是浪费的,因为如果失败是确定性的,那么就是在浪费时间重试你事先已知是致命的事情。
Spring Retry 提供了一些简单的通用无状态 RetryPolicy 实现(例如 SimpleRetryPolicy)以及前面示例中使用的 TimeoutRetryPolicy。
SimpleRetryPolicy 允许对指定的异常类型列表中的任何异常进行重试,最多重试固定次数。以下示例展示了如何使用它:
// Set the max attempts including the initial attempt before retrying
// and retry on all exceptions (this is the default):
SimpleRetryPolicy policy = new SimpleRetryPolicy(5, Collections.singletonMap(Exception.class, true));
// Use the policy...
RetryTemplate template = new RetryTemplate();
template.setRetryPolicy(policy);
template.execute(new RetryCallback<Foo>() {
public Foo doWithRetry(RetryContext context) {
// business logic here
}
});
还有一个更灵活的实现,称为 ExceptionClassifierRetryPolicy。
它允许你通过 ExceptionClassifier 抽象为任意一组异常类型配置不同的重试行为。该策略的工作方式是调用分类器将异常转换为委托 RetryPolicy。例如,可以将一种异常类型映射到不同的策略,从而使其在失败前比另一种异常类型重试更多次。
你可能需要实现自己的重试策略以实现更定制化的决策。例如,当存在一种众所周知的、特定于解决方案的异常分类,将异常分为可重试和不可重试时。
在瞬时故障后重试时,通常有助于在再次尝试之前等待一段时间,因为(通常)故障是由某些只能通过等待才能解决的问题引起的。如果 RetryCallback 失败,RetryTemplate 可以根据 BackoffPolicy 暂停执行。以下清单展示了 BackoffPolicy 接口的定义:
public interface BackoffPolicy {
BackOffContext start(RetryContext context);
void backOff(BackOffContext backOffContext)
throws BackOffInterruptedException;
}
BackoffPolicy 可以自由选择任何方式来实现退避。Spring Retry 提供的策略都使用 Object.wait()。一个常见的用例是采用指数级递增的等待时间进行退避,以避免两次重试进入同步状态并同时失败(这是从以太网中学到的教训)。为此,Spring Retry 提供了 ExponentialBackoffPolicy。Spring Retry 还提供了随机化的延迟策略,这些策略在复杂系统中避免相关故障之间产生共振时非常有用。
在多次不同的重试过程中,能够接收针对横切关注点的额外回调通常很有用。为此,Spring Retry 提供了 RetryListener 接口。RetryTemplate 允许你注册 RetryListener 实例,它们会收到带有 RetryContext 和 Throwable 的回调(在迭代过程中可用时)。
以下清单展示了 RetryListener 接口:
public interface RetryListener {