Spring-retry 使用指南

2022-12-02 17:01:02 浏览数 (1)

Spring-retry

文章目录

  • Spring-retry
    • 入门
      • 声明式示例
      • 命令式示例
    • 构建
    • 特性和API
      • RetryTemplate
      • RetryContext
      • RecoveryCallback
    • 无状态重试
    • 有状态重试
    • 重试策略
    • 回退策略
    • 监听器
      • 用于反射方法调用的监听器
    • 声明式重试
      • 用于重试代理的Java配置
        • 额外依赖项
      • XML配置

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

入门

声明式示例

代码语言:javascript复制
@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配置”部分。

命令式示例

代码语言:javascript复制
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(或更高)。

代码语言:javascript复制
$ mvn install

特性和API

RetryTemplate

为了使处理更健壮、更不容易失败,有时自动重试失败的操作会有所帮助,以防它在随后的尝试中可能成功,易受这种处理影响的错误本质上是暂时的。例如,对web服务或RMI服务的远程调用由于网络故障或数据库更新中的DeadLockLoserException而失败,可能在短时间的等待后自行解决,要自动化这些操作的重试,_Spring Retry_具有RetryOperations策略,RetryOperations接口看起来是这样的:

代码语言:javascript复制
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;

}

基本回调是一个简单的接口,允许你插入一些要重试的业务逻辑:

代码语言:javascript复制
public interface RetryCallback<T> {

    T doWithRetry(RetryContext context) throws Throwable;

}

执行回调,如果它失败(通过抛出Exception),将重试它,直到成功或实现决定中止为止。RetryOperations接口中有许多重载的execute方法,它们处理各种用例,以便在所有重试尝试都耗尽时进行恢复,还有重试状态,这允许客户端和实现在调用之间存储信息(稍后将详细介绍)。

RetryOperations最简单的通用实现是RetryTemplate,它可以这样用:

代码语言:javascript复制
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的流畅配置也可用:

代码语言:javascript复制
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,要使用此功能,客户端只需将回调函数一起传递给相同的方法,例如:

代码语言:javascript复制
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只允许对指定的异常类型列表中的任何一种进行重试,最多可以重试固定次数:

代码语言:javascript复制
// 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暂停执行。

代码语言:javascript复制
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

接口是这样的:

代码语言:javascript复制
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实现中详细检查方法调用的可能性。

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

代码语言:javascript复制
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,你还可以指定任意数量的重试监听器,例如:

代码语言:javascript复制
@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,例如:

代码语言:javascript复制
@Service
class Service {
    @Retryable(maxAttempts=12, backoff=@Backoff(delay=100, maxDelay=500))
    public service() {
        // ... do something
    }
}

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

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

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

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

代码语言:javascript复制
@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版引入了对某些属性使用表达式的功能:

代码语言:javascript复制
@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中添加以下行:

代码语言:javascript复制
runtime('org.springframework.boot:spring-boot-starter-aop')

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

代码语言:javascript复制
runtime('org.aspectj:aspectjweaver:1.8.13')

XML配置

下面是一个使用Spring AOP来重复对一个名为remoteCall的方法的服务调用的声明式迭代的例子(有关如何配置AOP拦截器的更多细节,请参阅Spring用户指南):

代码语言:javascript复制
<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实例注入拦截器。


作者其他热门文章推荐:

Java面试题专栏:

《从Java面试题看源码》-LongAdder、LongAccumulator是个什么东西?

《从Java面试题来看源码》-LinkedBlockingQueue 源码分析

《从Java面试题看源码》-有哪些并发队列?及ConcurrentLinkedQueue 源码分析

《从Java面试题看源码》-看完Kafka性能优化-让你吊打面试官

《从Java面试题看源码》-默认线程池阻塞队列为什么用LinkedBlockingQueue

Open JDK 11 源码阅读专栏

0 人点赞