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的存储策略,可以将其注入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,因此可以在此处注入有关限制和超时的常见问题(本章稍后将介绍)。

1.2.重试 Policy

RetryTemplate内部,由RetryPolicy决定execute方法重试还是失败的决定,它也是RetryContext的工厂。 RetryTemplate负责使用当前策略创建RetryContext,并在每次尝试时将其传递给RetryCallback。回调失败后,RetryTemplate必须调用RetryPolicy以要求其更新其状态(存储在RetryContext中),然后询问该策略是否可以进行另一次尝试。如果无法进行其他尝试(例如达到限制或检测到超时),则该策略还负责处理耗尽状态。简单的实现抛出RetryExhaustedException,这会导致任何封闭的事务被回滚。更复杂的实现可能会尝试采取一些恢复操作,在这种情况下,事务可以保持不变。

Tip

故障本质上是可以重试的,也可以是不可重试的。如果总是要从业务逻辑中抛出相同的异常,则重试它是没有好处的。因此,请勿重试所有异常类型。相反,请尝试仅关注那些您希望可重试的异常。主动重试通常对业务逻辑没有害处,但是却很浪费,因为如果确定性的失败,您将花费时间重试事先知道是致命的事情。

Spring Batch 提供了一些 Stateless 的RetryPolicy的简单通用实现,例如SimpleRetryPolicyTimeoutRetryPolicy(在前面的示例中使用)。

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,并在迭代过程中向他们提供RetryContextThrowable回调。

以下代码显示了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);
}

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

请注意,当侦听器不止一个时,它们在列表中,因此有一个 Sequences。在这种情况下,open的调用 Sequences 相同,而onErrorclose的调用 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的实例注入拦截器。