On this page
1. Retry
为了使处理过程更健壮且更不易出错,有时可以自动重试失败的操作,以防后续尝试成功执行。容易出现间歇性故障的错误通常是暂时性的。例如,由于网络故障或数据库更新中的DeadlockLoserDataAccessException
导致对 Web 服务的远程调用失败。
1.1. RetryTemplate
Note
从 2.2.0 版本开始,重试功能已从 Spring Batch 中撤出。现在,它是新库Spring Retry的一部分。
为了使重试操作自动化,Spring Batch 具有RetryOperations
策略。 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;
}
基本回调是一个简单的接口,可让您插入一些要重试的业务逻辑,如以下接口定义所示:
public interface RetryCallback<T, E extends Throwable> {
T doWithRetry(RetryContext context) throws E;
}
回调将运行,并且如果失败(通过抛出Exception
),则将重试该回调,直到成功或实现中止为止。 RetryOperations
接口中有许多重载的execute
方法。当所有重试尝试都用完并处理重试状态时,这些方法处理各种用例以进行恢复,这使 Client 端和实现可以在调用之间存储信息(我们将在本章稍后详细介绍)。
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.1.1. RetryContext
RetryCallback
的方法参数是RetryContext
。许多回调会忽略上下文,但如有必要,可以将其用作属性包,以在迭代期间存储数据。
如果在同一线程中正在进行嵌套重试,则RetryContext
具有父上下文。父上下文有时对于存储需要在execute
的调用之间共享的数据很有用。
1.1.2. RecoveryCallback
重试完成后,RetryOperations
可以将控制权传递给另一个回调,称为RecoveryCallback
。要使用此功能,Client 端将回调一起传递给相同的方法,如以下示例所示:
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
}
});
如果在模板决定中止之前业务逻辑未成功,则将使 Client 端有机会通过恢复回调进行一些替代处理。
1.1.3.Stateless 重试
在最简单的情况下,重试只是一个 while 循环。 RetryTemplate
可以 continue 尝试,直到成功或失败为止。 RetryContext
包含一些状态来确定是重试还是中止,但是此状态在堆栈上,不需要将其全局存储在任何地方,因此我们将其称为 Stateless 重试。Stateless 重试和有状态重试之间的区别包含在RetryPolicy
的实现中(RetryTemplate
可以同时处理两者)。在 Stateless 重试中,重试回调始终在失败时所处的同一线程中执行。
1.1.4.有状态重试
故障导致事务资源无效的地方,有一些特殊的注意事项。因为通常没有事务资源,所以这不适用于简单的远程调用,但是有时确实适用于数据库更新,尤其是在使用 Hibernate 时。在这种情况下,仅立即重新抛出称为故障的异常是有意义的,这样事务可以回滚并且我们可以开始一个新的有效事务。
在涉及事务的情况下,Stateless 重试是不够的,因为重新抛出和回滚必然涉及离开RetryOperations.execute()
方法并可能丢失堆栈中的上下文。为了避免丢失它,我们必须引入一种存储策略以将其从堆栈中取出并(至少)放入堆存储中。为此,Spring Batch 提供了一种名为RetryContextCache
的存储策略,可以将其注入RetryTemplate
。 RetryContextCache
的默认实现在内存中,使用简单的Map
。在群集环境中对多个进程进行高级使用时,可能还会考虑使用某种类型的群集缓存来实现RetryContextCache
(但是,即使在群集环境中,这也可能是过大的选择)。
RetryOperations
的部分职责是识别失败的操作,使其在新的执行中恢复(通常包含在新的事务中)。为方便起见,Spring Batch 提供了RetryState
抽象。这与RetryOperations
接口中的特殊execute
方法结合使用。
识别失败操作的方式是通过在重试的多次调用中识别状态。为了标识状态,用户可以提供一个RetryState
对象,该对象负责返回标识该 Item 的唯一键。该标识符在RetryContextCache
界面中用作键。
Warning
在RetryState
返回的键中对Object.equals()
和Object.hashCode()
的实现要非常小心。最好的建议是使用业务密钥来识别 Item。对于 JMS 消息,可以使用消息 ID。
重试用尽后,还可以选择以其他方式处理失败的 Item,而不是调用RetryCallback
(现在假定可能会失败)。就像在 Stateless 情况下一样,此选项由RecoveryCallback
提供,可以通过将其传递给RetryOperations
的execute
方法来提供。
重试或不重试的决定实际上是委托给常规的RetryPolicy
,因此可以在此处注入有关限制和超时的常见问题(本章稍后将介绍)。
1.2.重试 Policy
在RetryTemplate
内部,由RetryPolicy
决定execute
方法重试还是失败的决定,它也是RetryContext
的工厂。 RetryTemplate
负责使用当前策略创建RetryContext
,并在每次尝试时将其传递给RetryCallback
。回调失败后,RetryTemplate
必须调用RetryPolicy
以要求其更新其状态(存储在RetryContext
中),然后询问该策略是否可以进行另一次尝试。如果无法进行其他尝试(例如达到限制或检测到超时),则该策略还负责处理耗尽状态。简单的实现抛出RetryExhaustedException
,这会导致任何封闭的事务被回滚。更复杂的实现可能会尝试采取一些恢复操作,在这种情况下,事务可以保持不变。
Tip
故障本质上是可以重试的,也可以是不可重试的。如果总是要从业务逻辑中抛出相同的异常,则重试它是没有好处的。因此,请勿重试所有异常类型。相反,请尝试仅关注那些您希望可重试的异常。主动重试通常对业务逻辑没有害处,但是却很浪费,因为如果确定性的失败,您将花费时间重试事先知道是致命的事情。
Spring Batch 提供了一些 Stateless 的RetryPolicy
的简单通用实现,例如SimpleRetryPolicy
和TimeoutRetryPolicy
(在前面的示例中使用)。
SimpleRetryPolicy
允许重试任何命名类型的异常类型,最多固定次数。它还有一个“致命”异常列表,这些列表永远都不应重试,并且此列表会覆盖可重试列表,以便可以使用它来更好地控制重试行为,如以下示例所示:
SimpleRetryPolicy policy = new SimpleRetryPolicy();
// Set the max retry attempts
policy.setMaxAttempts(5);
// Retry on all exceptions (this is the default)
policy.setRetryableExceptions(new Class[] {Exception.class});
// ... but never retry IllegalStateException
policy.setFatalExceptions(new Class[] {IllegalStateException.class});
// 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
来起作用。例如,通过将异常类型 Map 到其他策略,可以在失败之前重试多次。
用户可能需要实施自己的重试策略以进行更多自定义的决策。例如,当对解决方案进行众所周知的,特定于解决方案的异常分类为可重试和不可重试时,自定义重试策略才有意义。
1.3.退避 Policy
在短暂故障后重试时,通常需要稍等一下再重试,因为通常故障是由某些问题引起的,只有通过 await 才能解决。如果RetryCallback
失败,则RetryTemplate
可以根据BackoffPolicy
暂停执行。
以下代码显示了BackOffPolicy
接口的接口定义:
public interface BackoffPolicy {
BackOffContext start(RetryContext context);
void backOff(BackOffContext backOffContext)
throws BackOffInterruptedException;
}
BackoffPolicy
可以自由选择以任何方式实施 backOff。 Spring Batch 提供的现成策略都使用Object.wait()
。一个常见的用例是使用成倍增加的 await 时间来回退,以避免两次重试进入锁定步骤而都失败(这是从以太网中学到的教训)。为此,Spring Batch 提供了ExponentialBackoffPolicy
。
1.4. Listeners
通常,能够接收更多的回调,以解决许多不同的重试中的交叉问题。为此,Spring Batch 提供了RetryListener
接口。 RetryTemplate
允许用户注册RetryListeners
,并在迭代过程中向他们提供RetryContext
和Throwable
回调。
以下代码显示了RetryListener
的接口定义:
public interface RetryListener {
<T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback);
<T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable);
<T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable);
}
在最简单的情况下,open
和close
回调在整个重试之前和之后出现,并且onError
适用于单个RetryCallback
调用。 close
方法可能还会收到Throwable
。如果发生错误,则是RetryCallback
抛出的最后一个错误。
请注意,当侦听器不止一个时,它们在列表中,因此有一个 Sequences。在这种情况下,open
的调用 Sequences 相同,而onError
和close
的调用 Sequences 相反。
1.5.声明式重试
有时,您知道某些业务处理每次发生时都想重试。典型的例子是远程服务调用。 Spring Batch 为此提供了一个 AOP 拦截器,该拦截器将方法调用包装在RetryOperations
实现中。 RetryOperationsInterceptor
根据提供的RetryTemplate
中的RetryPolicy
执行拦截的方法并重试失败。
以下示例显示了一个声明式重试,该重试性使用 Spring AOP 命名空间重试对名为remoteCall
的方法的服务调用(有关如何配置 AOP 拦截器的更多详细信息,请参见 Spring User Guide):
<aop:config>
<aop:pointcut id="transactional"
expression="execution(* com..*Service.remoteCall(..))" />
<aop:advisor pointcut-ref="transactional"
advice-ref="retryAdvice" order="-1"/>
</aop:config>
<bean id="retryAdvice"
class="org.springframework.retry.interceptor.RetryOperationsInterceptor"/>
以下示例显示了声明式重试,该声明重试使用 java 配置重试对名为remoteCall
的方法的服务调用(有关如何配置 AOP 拦截器的更多详细信息,请参见 Spring 用户指南):
@Bean
public MyService myService() {
ProxyFactory factory = new ProxyFactory(RepeatOperations.class.getClassLoader());
factory.setInterfaces(MyService.class);
factory.setTarget(new MyService());
MyService service = (MyService) factory.getProxy();
JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut();
pointcut.setPatterns(".*remoteCall.*");
RetryOperationsInterceptor interceptor = new RetryOperationsInterceptor();
((Advised) service).addAdvisor(new DefaultPointcutAdvisor(pointcut, interceptor));
return service;
}
前面的示例在拦截器内部使用默认值RetryTemplate
。要更改策略或侦听器,您可以将RetryTemplate
的实例注入拦截器。