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實例注入攔截器。

相關文章
相關標籤/搜索