# 重试

# 重试

XMLJavaBoth

为了使处理更健壮,更不容易失败,有时自动重试失败的操作会有所帮助,以防随后的尝试可能会成功。容易发生间歇性故障的错误通常是暂时的。例如,对 Web 服务的远程调用由于网络故障或数据库更新中的DeadlockLoserDataAccessException而失败。

# RetryTemplate

重试功能在 2.2.0 时从 Spring 批中退出。
它现在是一个新库Spring Retry (opens new window)的一部分。

要自动化重试操作 Spring,批处理有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;

}

Basic Callback 是一个简单的接口,允许你插入一些要重试的业务逻辑,如下面的接口定义所示:

public interface RetryCallback<T, E extends Throwable> {

    T doWithRetry(RetryContext context) throws E;

}

回调会运行,如果它失败(通过抛出Exception),则会重试它,直到它成功或实现中止为止。在RetryOperations接口中有许多重载的execute方法。当所有的重试尝试都用完时,这些方法处理用于恢复的各种用例,并处理重试状态,这使客户机和实现在调用之间存储信息(我们将在本章后面详细介绍这一点)。

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 服务调用,并将结果返回给用户。如果该调用失败,则重试该调用,直到达到超时为止。

# RetryContext

RetryCallback的方法参数是RetryContext。许多回调忽略了上下文,但如果有必要,它可以作为一个属性包来存储迭代期间的数据。

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

# RecoveryCallback

当重试用完时,RetryOperations可以将控制权传递给另一个回调,称为RecoveryCallback。要使用此功能,客户机将回调一起传递给相同的方法,如以下示例所示:

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

如果业务逻辑在模板决定中止之前没有成功,那么客户机将有机会通过恢复回调执行一些替代处理。

# 无状态重试

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

# 有状态重试

在故障导致事务资源无效的情况下,有一些特殊的考虑因素。这不适用于简单的远程调用,因为(通常)没有事务性资源,但有时确实适用于数据库更新,尤其是在使用 Hibernate 时。在这种情况下,只有立即重新抛出调用故障的异常才有意义,这样事务就可以回滚,并且我们可以启动一个新的有效事务。

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

RetryOperations的部分职责是识别在新执行中(并且通常包装在新事务中)返回的失败操作。为了促进这一点, Spring Batch 提供了RetryState抽象。这与RetryOperations接口中的特殊execute方法一起工作。

识别失败操作的方法是在重试的多个调用之间识别状态。为了识别状态,用户可以提供一个RetryState对象,该对象负责返回标识该项的唯一密钥。标识符在RetryContextCache接口中用作键。


中实现Object.equals()Object.hashCode()时要非常小心。最好的建议是使用业务键来标识
项。在 JMS 消息的情况下,可以使用消息 ID。

当重试用完时,还可以选择以不同的方式处理失败的项,而不是调用RetryCallback(现在认为很可能会失败)。就像在无状态的情况下一样,这个选项是由RecoveryCallback提供的,可以通过将其传递到executeRetryOperations方法来提供。

是否重试的决定实际上被委托给一个常规的RetryPolicy,因此通常对限制和超时的关注可以被注入到那里(在本章后面描述)。

# 重试策略

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

失败本质上要么是可重复的,要么是不可重复的。如果从业务逻辑中总是抛出相同的异常
,则重试没有好处。因此,不要重试所有
异常类型。相反,尝试只关注那些你期望
可重新尝试的异常。更积极地重试通常不会对业务逻辑造成损害,但是
这是浪费的,因为如果失败是确定性的,那么你将花费时间重试一些你事先知道是致命的

Spring 批处理提供了无状态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。例如,通过将一种异常类型映射到另一种策略,可以在失败前重试更多次。

用户可能需要实现他们自己的重试策略,以做出更多定制的决策。例如,当存在已知的、特定于解决方案的异常的可重试和不可重试的分类时,自定义重试策略是有意义的。

# 退避政策

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

下面的代码显示了BackOffPolicy接口的接口定义:

public interface BackoffPolicy {

    BackOffContext start(RetryContext context);

    void backOff(BackOffContext backOffContext)
        throws BackOffInterruptedException;

}

aBackoffPolicy可以自由地以它选择的任何方式实现退避。 Spring Batch Out of the Box 提供的策略都使用。一个常见的用例是后退,等待时间呈指数增长,以避免两次重试进入锁定步骤,两次都失败(这是从以太网学到的经验教训)。为此, Spring batch 提供了ExponentialBackoffPolicy

# 听众

通常情况下,能够接收跨多个不同重试中的交叉关注点的额外回调是有用的。为此, 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抛出的最后一个错误。

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

# 声明式重试

有时,你知道有些业务处理在每次发生时都想要重试。这方面的典型例子是远程服务调用。 Spring Batch 提供了 AOP 拦截器,该拦截器仅为此目的在RetryOperations实现中包装方法调用。根据提供的RepeatTemplate中的RetryPolicyRetryOperationsInterceptor执行截获的方法并在失败时重试。

下面的示例显示了一个声明性重试,它使用 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.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的实例注入拦截器。