9. Retry

9.1 RetryTemplate

Note

从 2.2.0 版本开始,重试功能已从 Spring Batch 中撤出。现在它是新库 Spring Retry 的一部分。

为了使处理过程更健壮且更不容易出错,有时可以自动重试失败的操作,以防后续尝试成功执行。易受此类处理影响的错误本质上是暂时的。例如,由于网络故障或数据库更新中的DeadLockLoserException而导致对 Web 服务或 RMI 服务的远程调用失败,可能会在短暂的 await 后解决。为了使此类操作自动重试,Spring Batch 具有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方法,它们处理所有重试尝试都已用尽时用于恢复的各种用例,并且还具有重试状态,这使 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 服务调用并将结果返回给用户。如果该呼叫失败,则重试直到超时。

9.1.1 RetryContext

RetryCallback的方法参数是RetryContext。许多回调将仅忽略上下文,但如有必要,可以将其用作属性包,以在迭代过程中存储数据。

如果在同一线程中正在进行嵌套重试,则RetryContext将具有父上下文。父上下文有时对于存储需要在execute的调用之间共享的数据很有用。

9.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 端有机会通过恢复回调进行一些替代处理。

9.1.3Stateless 重试

在最简单的情况下,重试只是一个 while 循环:RetryTemplate可以 continue 尝试直到成功或失败为止。 RetryContext包含一些状态来确定是重试还是中止,但是此状态在堆栈上,不需要将其全局存储在任何地方,因此我们将其称为 Stateless 重试。Stateless 重试和有状态重试之间的区别包含在RetryPolicy的实现中(RetryTemplate可以同时处理两者)。在 Stateless 重试中,回调总是在失败时在相同的线程中执行。

9.1.4 有状态重试

故障导致事务资源无效的地方,有一些特殊的注意事项。因为通常没有事务资源,所以这不适用于简单的远程调用,但是有时确实适用于数据库更新,尤其是在使用 Hibernate 时。在这种情况下,有意义的是立即重新抛出调用失败的异常,以便事务可以回滚并且我们可以开始一个新的有效事务。

在这些情况下,Stateless 重试不够好,因为重新抛出和回滚必然涉及离开RetryOperations.execute()方法并可能丢失堆栈中的上下文。为了避免丢失它,我们必须引入一种存储策略以将其从堆栈中取出并(至少)放入堆存储中。为此,Spring Batch 提供了一种存储策略RetryContextCache,可以将其注入RetryTemplateRetryContextCache的默认实现在内存中,使用简单的Map。在群集环境中对多个进程进行高级使用时,可能还会考虑使用某种类型的群集缓存来实现RetryContextCache(尽管,即使在群集环境中,这也可能会过大)。

RetryOperations的部分职责是识别失败的操作,使其在新的执行中恢复(通常包含在新的事务中)。为方便起见,Spring Batch 提供了RetryState抽象。这与RetryOperations中的特殊execute方法结合使用。

识别失败操作的方式是通过在重试的多次调用中识别状态。为了标识状态,用户可以提供一个RetryState对象,该对象负责返回标识该 Item 的唯一键。标识符用作RetryContextCache中的键。

Warning

RetryState返回的键中对Object.equals()Object.hashCode()的实现要非常小心。最好的建议是使用业务密钥来识别 Item。对于 JMS 消息,可以使用消息 ID。

重试用尽后,还可以选择以其他方式处理失败的 Item,而不是调用RetryCallback(现在假定它很可能失败)。就像在 Stateless 情况下一样,此选项由RecoveryCallback提供,可以通过将其传递给RetryOperationsexecute方法来提供。

是否重试的决定实际上是委托给常规的RetryPolicy,因此可以将有关限制和超时的常见问题注入那里(请参阅下文)。

9.2 重试策略

RetryTemplate内部,由RetryPolicy决定execute方法重试还是失败的决定,该RetryPolicy也是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 到其他策略相比,可以在失败之前重试一个异常类型多次。

用户可能需要实施自己的重试策略以进行更多自定义的决策。例如,如果存在众所周知的,特定于解决方案的 exception,则将异常分类为可重试和不可重试。

9.3 退避 Policy

在短暂故障后重试时,通常有助于稍等一会再尝试,因为通常故障是由某些问题引起的,只有通过 await 才能解决。如果RetryCallback失败,则RetryTemplate可以根据适当的BackoffPolicy暂停执行。

public interface BackoffPolicy {

    BackOffContext start(RetryContext context);

    void backOff(BackOffContext backOffContext)
        throws BackOffInterruptedException;

}

BackoffPolicy可以自由选择以任何方式实施 backOff。 Spring Batch 提供的现成策略都使用Object.wait()。一个常见的用例是使用成倍增加的 await 时间来回退,以避免两次重试进入锁定步骤而都失败了-这是从以太网中学到的教训。为此,Spring Batch 提供了ExponentialBackoffPolicy

9.4 Listeners

通常,能够收到更多的回调,以解决许多不同的重试中的交叉问题。为此,Spring Batch 提供了RetryListener接口。 RetryTemplate允许用户注册RetryListener,并且将在迭代过程中向他们提供RetryContextThrowable的回调。

界面如下所示:

public interface RetryListener {

    void open(RetryContext context, RetryCallback<T> callback);

    void onError(RetryContext context, RetryCallback<T> callback, Throwable e);

    void close(RetryContext context, RetryCallback<T> callback, Throwable e);
}

在最简单的情况下,openclose回调在整个重试之前和之后出现,并且onError适用于单个RetryCallback调用。 close方法可能还会收到Throwable;如果有错误,则是RetryCallback抛出的最后一个错误。

请注意,当有多个侦听器时,它们在列表中,因此有一个 Sequences。在这种情况下,open的调用 Sequences 相同,而onErrorclose的调用 Sequences 相反。

9.5 声明式重试

有时,您知道某些业务处理每次发生时都想重试。典型的例子是远程服务调用。 Spring Batch 为此提供了一个 AOP 拦截器,该方法将方法调用包装在RetryOperations中。 RetryOperationsInterceptor根据提供的RepeatTemplate中的RetryPolicy执行拦截的方法并重试失败。

这是一个使用 Spring AOP 名称空间重复声明称为remoteCall的服务的声明式迭代的示例(有关如何配置 AOP 拦截器的更多详细信息,请参见 Spring 用户指南):

<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.batch.retry.interceptor.RetryOperationsInterceptor"/>

上面的示例在拦截器内部使用默认的RetryTemplate。要更改策略或侦听器,您只需要将RetryTemplate的实例注入到拦截器中。