1. 重试


XML

Java

为了使处理更加健壮且不易出现故障,有时可以自动重试失败的操作,以防后续尝试成功。易受间歇性故障影响的错误通常是短暂的。示例包括 remote calls 到 web service,因为网络故障或数据库更新中的DeadlockLoserDataAccessException而失败。

1.1. RetryTemplate

从 2.2.0 开始,重试功能从 Spring Batch 中拉出。它现在是新 library 的一部分,Spring 重试。

要自动执行重试操作,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),它将被重试,直到它成功或 implementation 中止。 RetryOperations接口中有许多重载的execute方法。当所有重试尝试都用尽时,这些方法处理各种用于恢复的用例并处理重试 state,这允许 clients 和 implementations 在 calls 之间存储信息(我们将在本章后面详细介绍)。

RetryOperations的最简单的通用 implementation 是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;
    }

});

在前面的 example 中,我们进行 web service 调用并将结果返回给用户。如果该调用失败,则重试该调用直到达到超时。

1.1.1. RetryContext

RetryCallback的方法参数是RetryContext。许多回调忽略 context,但是,如果需要,它可以用作在迭代期间 store 数据的属性包。

如果在同一个线程中正在进行嵌套重试,则RetryContext具有 parent context。 parent context 偶尔用于存储需要在 calls 和execute之间共享的数据。

1.1.2. RecoveryCallback

重试耗尽时,RetryOperations可以将控制权传递给另一个名为RecoveryCallback的回调。要使用此 feature,clients 将回调一起传递给相同的方法,如下面的示例所示:

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. 无状态重试

在最简单的情况下,重试只是一个 while 循环。 RetryTemplate可以继续尝试,直到它成功或失败。 RetryContext包含一些 state 来确定是重试还是中止,但是这个 state 在堆栈上并且不需要在全局任何地方存储它,所以我们称之为 stateless 重试。 stateless 和有状态重试之间的区别包含在RetryPolicy的 implementation 中(RetryTemplate可以处理两者)。在 stateless 重试中,重试回调始终在失败时在其所在的同一线程中执行。

1.1.4. 有状态重试

如果失败导致 transactional 资源失效,则有一些特殊注意事项。这不适用于简单的 remote 调用,因为没有 transactional 资源(通常),但它有时适用于数据库更新,尤其是在使用 Hibernate 时。在这种情况下,只有 re-throw 立即调用失败的 exception 才有意义,这样 transaction 可以回滚,我们可以启动一个新的,有效的 transaction。

在涉及 transactions 的情况下,stateless 重试不够好,因为 re-throw 和回滚必然涉及离开RetryOperations.execute()方法并可能丢失堆栈上的 context。为了避免丢失它,我们必须引入一个存储策略来将其从堆栈中取出并将其(至少)放入堆存储中。为此,Spring Batch 提供了一个名为RetryContextCache的存储策略,可以将其注入RetryTemplate。使用简单的MapRetryContextCache的默认 implementation 在 memory 中。在集群环境中使用多个进程的高级用法也可以考虑使用某种 cluster 缓存来实现RetryContextCache(但是,即使在集群环境中,这可能也是过度的)。

RetryOperations的部分责任是在新的执行中返回失败的操作(并且通常包含在新的 transaction 中)。为了实现这一点,Spring Batch 提供了RetryState抽象。这与RetryOperations接口中的特殊execute方法结合使用。

识别失败操作的方式是通过在重试的多次调用中识别 state。要识别 state,用户可以提供RetryState object,负责返回标识 item 的唯一 key。标识符在RetryContextCache接口中用作 key。

RetryState返回的 key 中的Object.equals()Object.hashCode()的 implementation 非常小心。最好的建议是使用 business key 来识别项目。对于 JMS 消息,可以使用消息 ID。

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

重试与否的决定实际上被委托给常规的RetryPolicy,因此可以在那里注入关于限制和超时的常见问题(本章稍后将介绍)。

1.2. 重试 Policies

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

故障本质上是可重试的或不可重试的。如果总是会从业务逻辑中抛出相同的 exception,那么重试它就没有用了。所以不要重试所有 exception 类型。相反,尝试只关注那些您希望可以重试的 exceptions。通过更积极地重试业务逻辑通常不会有害,但这很浪费,因为如果失败是确定性的,那么你花费 time 重试一些你事先知道的事情是致命的。

Spring Batch 提供了 stateless RetryPolicy的一些简单的通用 implementations,例如SimpleRetryPolicyTimeoutRetryPolicy(在前面的 example 中使用)。

SimpleRetryPolicy允许在任何命名的 exception 类型列表上重试,最多固定次数。它还有一个永远不会重试的“致命”exceptions 列表,这个列表会覆盖可重试列表,以便它可以用来更好地控制重试行为,如下面的示例所示:

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
    }
});

还有一个更灵活的 implementation,称为ExceptionClassifierRetryPolicy,它允许用户通过ExceptionClassifier抽象为任意一组 exception 类型配置不同的重试行为。 policy 通过调用分类器将 exception 转换为委托RetryPolicy来工作。对于 example,通过将一个 exception 类型映射到另一个 policy,可以在失败之前重试多次 exception 类型。

用户可能需要实施自己的重试 policies 以获得更多自定义决策。例如,当_ex_,分类为 exceptions 可重试且不可重试时,自定义重试 policy 是有意义的。

1.3. 退避政策

在暂时故障后重试时,通常会在再次尝试之前等待一段时间,因为通常故障是由某些问题引起的,只能通过等待来解决。如果RetryCallback失败,RetryTemplate可以根据BackoffPolicy暂停执行。

以下 code 显示BackOffPolicy接口的接口定义:

public interface BackoffPolicy {

    BackOffContext start(RetryContext context);

    void backOff(BackOffContext backOffContext)
        throws BackOffInterruptedException;

}

BackoffPolicy可以以任何方式自由实现 backOff。由 Spring Batch 开箱提供的 policies 都使用Object.wait()。一个 common 用例是以指数级增加的等待时间退避,以避免两次重试进入锁定 step 并且都失败(这是从以太网学到的教训)。为此,Spring Batch 提供了ExponentialBackoffPolicy

1.4. 听众

通常,能够在许多不同的重试中接收针对横切关注点的额外回调是有用的。为此,Spring Batch 提供RetryListener接口。 RetryTemplate允许用户注册RetryListeners,并且在迭代期间可以使用RetryContextThrowable给出回调。

以下 code 显示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 calls。 close方法也可能会收到Throwable。如果出现错误,则是RetryCallback抛出的最后一个错误。

请注意,当有多个 listener 时,它们位于列表中,因此存在 order。在这种情况下,open在同一 order 中调用,而onErrorclose在 reverse order 中调用。

1.5. 声明性重试

有时候,有一些业务处理,你知道你想要在每次发生的时候重试。经典示例是 remote 服务调用。 Spring Batch 提供了一个 AOP 拦截器,它为RetryOperations implementation 包装一个方法调用,仅用于此目的。 RetryOperationsInterceptor执行截获的方法,并根据提供的RetryTemplate中的RetryPolicy重试失败。

以下 example 显示了一个声明性重试,它使用 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"/>

以下 example 显示了一个声明性重试,它使用 java configuration 重试对名为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;
}

前面的 example 在拦截器中使用默认的RetryTemplate。要更改 policies 或 listeners,可以将RetryTemplate的实例注入拦截器。