Spring 指南(spring-retry)

spring-retry

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

入门

声明式示例

@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注解属性中有各类选项,用于包含和排除异常类型、限制重试次数和回退策略。spring

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

命令式示例

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

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

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

构建

要求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;

}

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

public interface RetryCallback<T> {

    T doWithRetry(RetryContext context) throws Throwable;

}

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

RetryOperations最简单的通用实现是RetryTemplate,它能够这样用:ide

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实例注入拦截器。

相关文章
相关标签/搜索