9. 重试

9.1 RetryTemplate

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

为了使处理更加健壮并且更不容易出现故障,有时它会自动重试失败的操作,以防它在后续尝试中成功。易受这种治疗影响的错误本质上是短暂的。对于 example,对 web service 或 RMI 服务的 remote 调用由于网络故障或数据库更新中的DeadLockLoserException而失败,可能会在短暂的等待后自行解决。要自动重试此类操作,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),它将被重试,直到它成功,或者 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 调用并将结果返回给用户。如果该调用失败,则重试该调用,直到达到超时。

9.1.1 RetryContext

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

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

9.1.2 RecoveryCallback

当重试耗尽时,RetryOperations可以将控制传递给另一个回调RecoveryCallback。要使用此 feature clients,只需将回调传递到同一方法,对于 example:

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

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

9.1.4 有状态重试

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

在这些情况下,stateless 重试不够好,因为 re-throw 和回滚必然涉及离开RetryOperations.execute()方法并可能丢失堆栈上的 context。为了避免丢失它,我们必须引入一个存储策略来将其从堆栈中取出并将其(至少)放入堆存储中。为此,Spring Batch 提供了一个可以注入RetryTemplate的存储策略RetryContextCache。使用简单的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,因此可以在那里注入关于限制和超时的常见问题(见下文)。

9.2 重试 Policies

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

失败本身就是可以重试的 - 如果总是从业务逻辑中抛出相同的 exception,则无法重试它。所以不要重试所有 exception 类型 - 尝试只关注那些你希望可以重试的 exceptions。更积极地重试业务逻辑通常不会有害,但这是浪费,因为如果失败是确定性的,那么 time 会重新尝试事先知道的事情是致命的。

Spring Batch 提供了 stateless RetryPolicy的一些简单的通用 implementations,用于 example a SimpleRetryPolicy,以及TimeoutRetryPolicy用于上面的 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。

用户可能需要实施自己的重试 policies 以获得更多自定义决策。例如,如果存在 well-known,solution-specific,则将 exceptions 分类为可重试且不可重试。

9.3 退避政策

在暂时故障后重试时,通常会在再次尝试之前等待一段时间,因为通常故障是由某些问题引起的,只能通过等待来解决。如果RetryCallback失败,则RetryTemplate可以根据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

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

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

9.5 声明性重试

有时会有一些业务处理,你知道你想要在每次发生的时候重试。经典示例是 remote 服务调用。 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"/>

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

Updated at: 9 months ago
8.6. 声明性迭代Table of content10. 单元测试