Spring-指南springretry

10次阅读

共计 10126 个字符,预计需要花费 26 分钟才能阅读完成。

spring-retry

该项目为 Spring 应用程序提供声明式重试支持,它用于 Spring Batch、Spring Integration、Apache Hadoop 的 Spring(以及其他),命令式重试也支持显式使用。

入门

声明式示例

@Configuration
@EnableRetry
public class Application {

    @Bean
    public Service service() {return new Service();
    }

}

@Service
class Service {@Retryable(RemoteAccessException.class)
    public void service() {// ... do something}
    @Recover
    public void recover(RemoteAccessException e) {// ... panic}
}

调用 service 方法,如果它由于 RemoteAccessException 失败,那么它将重试(默认情况下最多三次),如果继续失败,则执行 recover 方法,@Retryable注解属性中有各种选项,用于包含和排除异常类型、限制重试次数和回退策略。

使用上面显示的 @Retryable 注解应用重试处理的声明式方法对 AOP 类有一个额外的运行时依赖,有关如何解决项目中的这种依赖关系的详细信息,请参阅下面的“重试代理的 Java 配置”部分。

命令式示例

RetryTemplate template = RetryTemplate.builder()
                .maxAttempts(3)
                .fixedBackoff(1000)
                .retryOn(RemoteAccessException.class)
                .build();

template.execute(ctx -> {// ... do something});

旧版本:参见 RetryTemplate 部分中的示例。

构建

要求 Java 1.7 和 Maven 3.0.5(或更高)。

$ mvn install

特性和 API

RetryTemplate

为了使处理更健壮、更不容易失败,有时自动重试失败的操作会有所帮助,以防它在随后的尝试中可能成功,易受这种处理影响的错误本质上是暂时的。例如,对 web 服务或 RMI 服务的远程调用由于网络故障或数据库更新中的 DeadLockLoserException 而失败,可能在短时间的等待后自行解决,要自动化这些操作的重试,Spring Retry具有 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 方法,它们处理各种用例,以便在所有重试尝试都耗尽时进行恢复,还有重试状态,这允许客户端和实现在调用之间存储信息(稍后将详细介绍)。

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.3 版开始,RetryTemplate的流畅配置也可用:

RetryTemplate.builder()
      .maxAttempts(10)
      .exponentialBackoff(100, 2, 10000)
      .retryOn(IOException.class)
      .traversingCauses()
      .build();
 
RetryTemplate.builder()
      .fixedBackoff(10)
      .withinMillis(3000)
      .build();
 
RetryTemplate.builder()
      .infiniteRetry()
      .retryOn(IOException.class)
      .uniformRandomBackoff(1000, 3000)
      .build();

RetryContext

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

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

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 Retry提供了一种存储策略 RetryContextCache,可以将其注入RetryTemplateRetryContextCache 的默认实现在内存中,使用一个简单的Map,它有一个严格执行的最大容量,以避免内存泄漏,但它没有任何高级缓存功能,如生存时间。如果需要,应该考虑注入具有这些特性的Map,在集群环境中对多个进程的高级使用可能还会考虑使用某种集群缓存实现RetryContextCache(不过,即使在集群环境中,这也可能是多余的)。

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

识别失败操作的方法是跨重试的多个调用标识状态,要标识状态,用户可以提供 RetryState 对象,该对象负责返回标识该项的唯一键,标识符用作 RetryContextCache 中的键。

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

当重试耗尽时,还可以选择以另一种方式处理失败的项,而不是调用 RetryCallback(现在假定很可能会失败),就像在无状态的情况下一样,这个选项是由RecoveryCallback 提供的,它可以通过将其传递给 RetryOperationsexecute方法来提供。

重试或不重试的决定实际上委托给了一个常规的RetryPolicy,因此可以在那里注入对限制和超时的常见关注(参见下面)。

重试策略

RetryTemplate 中,execute方法中重试或失败的决定由 RetryPolicy 决定,RetryPolicy也是 RetryContext 的工厂。RetryTemplate有责任使用当前策略创建 RetryContext,并在每次尝试时将其传递给RetryCallback。回调失败后,RetryTemplate 必须调用 RetryPolicy 来要求它更新状态(该状态将存储在 RetryContext 中),然后它询问策略是否可以进行另一次尝试。如果无法进行另一次尝试(例如达到限制或检测到超时),则策略还负责标识耗尽状态,但不负责处理异常。RetryTemplate将抛出原始异常,除非在有状态的情况下,当没有可用的恢复,在这种情况下,它将抛出RetryExhaustedException。你还可以在 RetryTemplate 中设置一个标志,让它无条件地从回调(即从用户代码)抛出原始异常。

失败本质上要么是可重试的,要么是不可重试的 — 如果总是要从业务逻辑中抛出相同的异常,那么重试是没有帮助的。所以不要在所有异常类型上重试 — 试着只关注那些你希望可以重试的异常。更积极地重试通常不会对业务逻辑造成损害,但这是浪费,因为如果失败是确定的,那么重试一些预先知道是致命的东西就会花费时间。

Spring Retry提供了一些无状态 RetryPolicy 的简单通用实现,例如 SimpleRetryPolicy 和上面示例中使用的TimeoutRetryPolicy

SimpleRetryPolicy只允许对指定的异常类型列表中的任何一种进行重试,最多可以重试固定次数:

// Set the max attempts including the initial attempt before retrying
// and retry on all exceptions (this is the default):
SimpleRetryPolicy policy = new SimpleRetryPolicy(5, Collections.singletonMap(Exception.class, true));

// 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 暂停执行。

public interface BackoffPolicy {BackOffContext start(RetryContext context);

    void backOff(BackOffContext backOffContext)
        throws BackOffInterruptedException;

}

回退策略可以自由地以其选择的任何方式实现回退,Spring Retry开箱即用提供的策略都使用 Thread.sleep()。一个常见的用例是用指数级增长的等待时间来回退,以避免两次重试进入锁步,两次都失败 — 这是从以太网中学到的教训。为此,Spring Retry 提供了ExponentialBackoffPolicy,还有一些随机版本的延迟策略,对于避免在复杂系统中的相关故障之间产生共振非常有用。

监听器

对于跨多个不同重试的横切关注点,能够接收额外的回调通常是有用的,为此,Spring Retry提供了 RetryListener 接口,RetryTemplate允许用户注册 RetryListeners,在迭代期间,他们将使用RetryContext 获得回调,并在可用的地方使用Throwable

接口是这样的:

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 抛出的最后一个错误。

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

用于反射方法调用的监听器

当处理用 @Retryable 注解的方法或用 Spring AOP 拦截的方法时,spring-retry提供了在 RetryListener 实现中详细检查方法调用的可能性。

当需要监视某个方法调用被重试的频率并使用详细的标记信息(例如:类名、方法名,甚至在某些特殊情况下的参数值)公开它时,这种场景可能特别有用。

template.registerListener(new MethodInvocationRetryListenerSupport() {
      @Override
      protected <T, E extends Throwable> void doClose(RetryContext context,
          MethodInvocationRetryCallback<T, E> callback, Throwable throwable) {monitoringTags.put(labelTagName, callback.getLabel());
        Method method = callback.getInvocation()
            .getMethod();
        monitoringTags.put(classTagName,
            method.getDeclaringClass().getSimpleName());
        monitoringTags.put(methodTagName, method.getName());

        // register a monitoring counter with appropriate tags
        // ...
      }
    });

声明式重试

有时候,有些业务处理你知道每次发生时都要重试,这方面的经典示例是远程服务调用,Spring Retry提供了一个 AOP 拦截器,它将方法调用封装在 RetryOperations 中正是出于这个目的。RetryOperationsInterceptor执行拦截方法,并根据所提供的 RetryTemplate 中的 RetryPolicy 在失败时重试。

用于重试代理的 Java 配置

@EnableRetry 注解添加到你的 @Configuration 类之一,并在要重试的方法(或所有方法的类型级别)上使用@Retryable,你还可以指定任意数量的重试监听器,例如:

@Configuration
@EnableRetry
public class Application {

    @Bean
    public Service service() {return new Service();
    }
    
    @Bean public RetryListener retryListener1() {return new RetryListener() {...}
    }
    
    @Bean public RetryListener retryListener2() {return new RetryListener() {...}
    }

}

@Service
class Service {@Retryable(RemoteAccessException.class)
    public service() {// ... do something}
}

@Retryable的属性可以用来控制 RetryPolicyBackoffPolicy,例如:

@Service
class Service {@Retryable(maxAttempts=12, backoff=@Backoff(delay=100, maxDelay=500))
    public service() {// ... do something}
}

100500毫秒之间进行随机回退,最多尝试 12 次,还有一个 stateful 属性(默认为false)来控制重试是否有状态,要使用有状态重试,拦截方法必须有参数,因为它们用于构造状态的缓存键。

@EnableRetry注解还查找类型为 Sleeperbean,以及 RetryTemplate 和拦截器中用于控制运行时重试行为的其他策略。

@EnableRetry注解为 @Retryable bean 创建代理,代理(应用程序中的 bean 实例)中添加了 Retryable 接口,这纯粹是一个标记接口,但对于希望应用重试建议的其他工具可能很有用(如果 bean 已经实现了Retryable,那么它们通常不需要麻烦)。

可以提供恢复方法,以便在重试耗尽时采用另一种代码路径,方法应该与 @Retryable 在同一个类中声明,并标记为 @Recover,返回类型必须匹配@Retryable 方法。恢复方法的参数可以有选择地包括抛出的异常,也可以有选择地包括传递给原始 retryable 方法的参数(或者它们的部分列表,只要没有一个被省略),例如:

@Service
class Service {@Retryable(RemoteAccessException.class)
    public void service(String str1, String str2) {// ... do something}
    @Recover
    public void recover(RemoteAccessException e, String str1, String str2) {// ... error handling making use of original args if required}
}

1.2 版引入了对某些属性使用表达式的功能:

@Retryable(exceptionExpression="message.contains('this can be retried')")
public void service1() {...}

@Retryable(exceptionExpression="message.contains('this can be retried')")
public void service2() {...}

@Retryable(exceptionExpression="@exceptionChecker.shouldRetry(#root)",
    maxAttemptsExpression = "#{@integerFiveBean}",
  backoff = @Backoff(delayExpression = "#{1}", maxDelayExpression = "#{5}", multiplierExpression = "#{1.1}"))
public void service3() {...}

Spring Retry 1.2.5,对于exceptionExpression,不推荐使用模板表达式(#{...}),而支持简单表达式字符串(message.contains('this can be retried'))。

表达式可以包含属性占位符,比如 #{${max.delay}}#{@exceptionChecker.${retry.method}(#root)}

  • exceptionExpression作为 #root 对象对抛出的异常求值。
  • maxAttemptsExpression@BackOff 表达式属性在初始化期间只计算一次,没有用于计算的根对象,但是它们可以在上下文中引用其他bean

额外依赖项

使用上面显示的 @Retryable 注解应用重试处理的声明式方法对 AOP 类有额外的运行时依赖性,需要在项目中声明这些类,如果你的应用程序是使用 Spring Boot 实现的,那么最好使用 AOP 的 Spring Boot starter 解决这个依赖关系,例如,对于 Gradle,在 build.gradle 中添加以下行:

runtime('org.springframework.boot:spring-boot-starter-aop')

对于非 Boot 应用程序,声明运行时依赖于 AspectJ 的 aspectjweaver 模块的最新版本,例如,对于 Gradle,在 build.gradle 中添加以下行:

runtime('org.aspectj:aspectjweaver:1.8.13')

XML 配置

下面是一个使用 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"/>

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


正文完
 0