1. 程式人生 > >spring-retry重試與熔斷詳解— 億級流量 內容補充

spring-retry重試與熔斷詳解— 億級流量 內容補充

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow

也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!

               

  本文是《億級流量網站架構核心技術——跟開濤學搭建高可用高併發系統》第6章 超時與重試機制補充內容。
  spring-retry專案實現了重試和熔斷功能,目前已用於SpringBatch、Spring Integration等專案。
                 


  RetryOperations定義了重試的API,RetryTemplate提供了模板實現,執行緒安全的,同於Spring 一貫的API風格,RetryTemplate將重試、熔斷功能封裝到模板中,提供健壯和不易出錯的API供大家使用。
  首先,RetryOperations介面API:

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, RetryStateretryState)         throws E
;}

  通過RetryCallback定義需重試的業務服務,當重試超過最大重試時間或最大重試次數後可以呼叫RecoveryCallback進行恢復,比如返回假資料或託底資料。
  那什麼時候需重試?spring-retry是當丟擲相關異常後執行重試策略,定義重試策略時需要定義需重試的異常(如因遠端呼叫失敗的可以重試、而因入參校對失敗不應該重試)。只讀操作可以重試,冪等寫操作可以重試,但是非冪等寫操作不能重試,重試可能導致髒寫,或產生重複資料。
  重試策略有哪些呢?spring-retry提供瞭如下重試策略。
           
  RetryPolicy提供瞭如下策略實現:  

  • NeverRetryPolicy:只允許呼叫RetryCallback一次,不允許重試;
  • AlwaysRetryPolicy:允許無限重試,直到成功,此方式邏輯不當會導致死迴圈;
  • SimpleRetryPolicy:固定次數重試策略,預設重試最大次數為3次,RetryTemplate預設使用的策略;
  • TimeoutRetryPolicy:超時時間重試策略,預設超時時間為1秒,在指定的超時時間內允許重試;
  • CircuitBreakerRetryPolicy:有熔斷功能的重試策略,需設定3個引數openTimeout、resetTimeout和delegate,稍後詳細介紹該策略;
  • CompositeRetryPolicy:組合重試策略,有兩種組合方式,樂觀組合重試策略是指只要有一個策略允許重試即可以,悲觀組合重試策略是指只要有一個策略不允許重試即可以,但不管哪種組合方式,組合中的每一個策略都會執行。

重試時的退避策略是什麼?是立即重試還是等待一段時間後重試,比如是網路錯誤,立即重試將導致立即失敗,最好等待一小段時間後重試,還要防止很多服務同時重試導致DDos。
           
  BackOffPolicy 提供瞭如下策略實現:

  • NoBackOffPolicy:無退避演算法策略,即當重試時是立即重試;
  • FixedBackOffPolicy:固定時間的退避策略,需設定引數sleeper和backOffPeriod,sleeper指定等待策略,預設是Thread.sleep,即執行緒休眠,backOffPeriod指定休眠時間,預設1秒;
  • UniformRandomBackOffPolicy:隨機時間退避策略,需設定sleeper、minBackOffPeriod和maxBackOffPeriod,該策略在[minBackOffPeriod,maxBackOffPeriod之間取一個隨機休眠時間,minBackOffPeriod預設500毫秒,maxBackOffPeriod預設1500毫秒;
  • ExponentialBackOffPolicy:指數退避策略,需設定引數sleeper、initialInterval、maxInterval和multiplier,initialInterval指定初始休眠時間,預設100毫秒,maxInterval指定最大休眠時間,預設30秒,multiplier指定乘數,即下一次休眠時間為當前休眠時間*multiplier;
  • ExponentialRandomBackOffPolicy:隨機指數退避策略,引入隨機乘數,之前說過固定乘數可能會引起很多服務同時重試導致DDos,使用隨機休眠時間來避免這種情況。

到此基本的概念就講完了。接下來先看下RetryTemplate主要流程實現:

protected <T, E extends Throwable> T doExecute(RetryCallback<T, E> retryCallback,      RecoveryCallback<T> recoveryCallback, RetryState state)      throws E, ExhaustedRetryException {   //重試策略   RetryPolicy retryPolicy = this.retryPolicy;   //退避策略   BackOffPolicy backOffPolicy = this.backOffPolicy;   //重試上下文,當前重試次數等都記錄在上下文中   RetryContext context = open(retryPolicy, state);   try {      //攔截器模式,執行RetryListener#open      boolean running = doOpenInterceptors(retryCallback, context);      //判斷是否可以重試執行      while (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) {         try {//執行RetryCallback回撥            return retryCallback.doWithRetry(context);         } catch (Throwable e) {//異常時,要進行下一次重試準備            //遇到異常後,註冊該異常的失敗次數            registerThrowable(retryPolicy, state, context, e);            //執行RetryListener#onError            doOnErrorInterceptors(retryCallback, context, e);            //如果可以重試,執行退避演算法,比如休眠一小段時間後再重試            if (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) {               backOffPolicy.backOff(backOffContext);            }            //state != null && state.rollbackFor(context.getLastThrowable())            //在有狀態重試時,如果是需要執行回滾操作的異常,則立即丟擲異常            if (shouldRethrow(retryPolicy, context, state)) {               throw RetryTemplate.<E>wrapIfNecessary(e);            }         }         //如果是有狀態重試,且有GLOBAL_STATE屬性,則立即跳出重試終止;當丟擲的異常是非需要執行回滾操作的異常時,才會執行到此處,CircuitBreakerRetryPolicy會在此跳出迴圈;         if (state != null && context.hasAttribute(GLOBAL_STATE)) {            break;         }      }      //重試失敗後,如果有RecoveryCallback,則執行此回撥,否則丟擲異常      return handleRetryExhausted(recoveryCallback, context, state);   } catch (Throwable e) {      throw RetryTemplate.<E>wrapIfNecessary(e);   } finally {      //清理環境      close(retryPolicy, context, state, lastException == null || exhausted);      //執行RetryListener#close,比如統計重試資訊      doCloseInterceptors(retryCallback, context, lastException);   }}

有狀態or無狀態

  無狀態重試,是在一個迴圈中執行完重試策略,即重試上下文保持在一個執行緒上下文中,在一次呼叫中進行完整的重試策略判斷。
  非常簡單的情況,如遠端呼叫某個查詢方法時是最常見的無狀態重試。

RetryTemplate template = new RetryTemplate();//重試策略:次數重試策略RetryPolicy retryPolicy = new SimpleRetryPolicy(3);template.setRetryPolicy(retryPolicy);//退避策略:指數退避策略ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();backOffPolicy.setInitialInterval(100);backOffPolicy.setMaxInterval(3000);backOffPolicy.setMultiplier(2);backOffPolicy.setSleeper(new ThreadWaitSleeper());template.setBackOffPolicy(backOffPolicy);//當重試失敗後,丟擲異常String result = template.execute(new RetryCallback<String, RuntimeException>() {    @Override    public String doWithRetry(RetryContext context) throws RuntimeException {        throw new RuntimeException("timeout");    }});//當重試失敗後,執行RecoveryCallbackString result = template.execute(new RetryCallback<String, RuntimeException>() {    @Override    public String doWithRetry(RetryContext context) throws RuntimeException {        System.out.println("retry count:" + context.getRetryCount());        throw new RuntimeException("timeout");    }}, new RecoveryCallback<String>() {    @Override    public String recover(RetryContext context) throws Exception {        return "default";    }});

  有狀態重試,有兩種情況需要使用有狀態重試,事務操作需要回滾或者熔斷器模式。 
  事務操作需要回滾場景時,當整個操作中丟擲的是資料庫異常DataAccessException,則不能進行重試需要回滾,而丟擲其他異常則可以進行重試,可以通過RetryState實現:

//當前狀態的名稱,當把狀態放入快取時,通過該key查詢獲取Object key = "mykey";//是否每次都重新生成上下文還是從快取中查詢,即全域性模式(如熔斷器策略時從快取中查詢)boolean isForceRefresh = true;//對DataAccessException進行回滾BinaryExceptionClassifier rollbackClassifier =        new BinaryExceptionClassifier(Collections.<Class<? extends Throwable>>singleton(DataAccessException.class));RetryState state = new DefaultRetryState(key, isForceRefresh, rollbackClassifier);String result = template.execute(new RetryCallback<String, RuntimeException>() {    @Override    public String doWithRetry(RetryContext context) throws RuntimeException {        System.out.println("retry count:" + context.getRetryCount());        throw new TypeMismatchDataAccessException("");    }}, new RecoveryCallback<String>() {    @Override    public String recover(RetryContext context) throws Exception {        return "default";    }}, state);

  RetryTemplate中在有狀態重試時,回滾場景時直接丟擲異常處理程式碼:

//state != null && state.rollbackFor(context.getLastThrowable())//在有狀態重試時,如果是需要執行回滾操作的異常,則立即丟擲異常if (shouldRethrow(retryPolicy,context, state)) {    throw RetryTemplate.<E>wrapIfNecessary(e);}

  熔斷器場景。在有狀態重試時,且是全域性模式,不在當前迴圈中處理重試,而是全域性重試模式(不是執行緒上下文),如熔斷器策略時測試程式碼如下所示。

RetryTemplate template = new RetryTemplate();CircuitBreakerRetryPolicy retryPolicy =        new CircuitBreakerRetryPolicy(new SimpleRetryPolicy(3));retryPolicy.setOpenTimeout(5000);retryPolicy.setResetTimeout(20000);template.setRetryPolicy(retryPolicy);for (int i = 0; i < 10; i++) {    try {        Object key = "circuit";        boolean isForceRefresh = false;        RetryState state = new DefaultRetryState(key, isForceRefresh);        String result = template.execute(new RetryCallback<String, RuntimeException>() {            @Override            public String doWithRetry(RetryContext context) throws RuntimeException {                System.out.println("retry count:" + context.getRetryCount());                throw new RuntimeException("timeout");            }        }, new RecoveryCallback<String>() {            @Override            public String recover(RetryContext context) throws Exception {                return "default";            }        }, state);        System.out.println(result);    } catch (Exception e) {        System.out.println(e);    }}

  為什麼說是全域性模式呢?我們配置了isForceRefresh為false,則在獲取上下文時是根據key “circuit”從快取中獲取,從而拿到同一個上下文。

Object key = "circuit";boolean isForceRefresh = false;RetryState state = new DefaultRetryState(key,isForceRefresh);如下RetryTemplate程式碼說明在有狀態模式下,不會在迴圈中進行重試。if (state != null && context.hasAttribute(GLOBAL_STATE)) {   break;}

  熔斷器策略配置程式碼,CircuitBreakerRetryPolicy需要配置三個引數:

  • delegate:是真正判斷是否重試的策略,當重試失敗時,則執行熔斷策略;
  • openTimeout:openWindow,配置熔斷器電路開啟的超時時間,當超過openTimeout之後熔斷器電路變成半開啟狀態(主要有一次重試成功,則閉合電路); resetTimeout:timeout,配置重置熔斷器重新閉合的超時時間。

判斷熔斷器電路是否開啟的程式碼:

public boolean isOpen() {   long time = System.currentTimeMillis() - this.start;   boolean retryable = this.policy.canRetry(this.context);   if (!retryable) {//重試失敗      //在重置熔斷器超時後,熔斷器器電路閉合,重置上下文      if (time > this.timeout) {         this.context = createDelegateContext(policy, getParent());         this.start = System.currentTimeMillis();         retryable = this.policy.canRetry(this.context);      } else if (time < this.openWindow) {         //當在熔斷器開啟狀態時,熔斷器電路開啟,立即熔斷         if ((Boolean) getAttribute(CIRCUIT_OPEN) == false) {            setAttribute(CIRCUIT_OPEN, true);         }         this.start = System.currentTimeMillis();         return true;      }   } else {//重試成功      //在熔斷器電路半開啟狀態時,斷路器電路閉合,重置上下文      if (time > this.openWindow) {         this.start = System.currentTimeMillis();         this.context = createDelegateContext(policy, getParent());      }   }   setAttribute(CIRCUIT_OPEN, !retryable);   return !retryable;}

  從如上程式碼可看出spring-retry的熔斷策略相對簡單:

  • 當重試失敗,且在熔斷器開啟時間視窗[0,openWindow) 內,立即熔斷;
  • 當重試失敗,且在指定超時時間後(>timeout),熔斷器電路重新閉合;
  • 在熔斷器半開啟狀態[openWindow, timeout] 時,只要重試成功則重置上下文,斷路器閉合。

CircuitBreakerRetryPolicy的delegate應該配置基於次數的SimpleRetryPolicy或者基於超時的TimeoutRetryPolicy策略,且策略都是全域性模式,而非區域性模式,所以要注意次數或超時的配置合理性。 
  比如SimpleRetryPolicy配置為3次,openWindow=5s,timeout=20s,我們來看下CircuitBreakerRetryPolicy的極端情況。
             
  特殊時間序列:

  • 1s:retryable=false,重試失敗,斷路器電路處於開啟狀態,熔斷,重置start時間為當前時間;
  • 2s:retryable=false,重試失敗,斷路器電路處於開啟狀態,熔斷,重置start時間為當前時間;
  • 7s:retryable=true,表示可以重試,但是time=5s,time > this.openWindow判斷為false,CIRCUIT_OPEN=false,不熔斷;此時重試次數=3,等於最大重試次數了;
  • 10s:retryable=false,因重試次數>3,time=8s,time < this.openWindow判斷為false,熔斷,且在timeout超時之前都處於熔斷狀態,這個時間段要配置好,否則熔斷的時間會太長(預設timeout=20s);
  • (7s,20s]之間的所有重試:和10s的情況一樣。

如上是當重試次數正好等於最大重試次數,且time=openWindow時的特殊情況,不過實際場景這種情況幾乎不可能發生。 
  spring-retry的重試機制沒有像Hystrix根據失敗率閥值進行電路開啟/關閉的判斷。 
  如果需要區域性迴圈重試機制,需要組合多個RetryTemplate實現。 
  spring-retry也提供了註解實現:
  @EnableRetry、@Retryable、@Recover、@Backoff、@CircuitBreaker。具體可以參考官方文件。

統計分析

  spring-retry通過RetryListener實現攔截器模式,預設提供了StatisticsListener實現重試操作統計分析資料。

RetryTemplatetemplate = new RetryTemplate();DefaultStatisticsRepository repository = new DefaultStatisticsRepository();StatisticsListener listener = new StatisticsListener(repository);template.setListeners(new RetryListener[]{listener});for (int i = 0; i < 10; i++){    String result = template.execute(new RetryCallback<String, RuntimeException>() {        @Override       public String doWithRetry(RetryContext context) throws RuntimeException {           context.setAttribute(RetryContext.NAME,"method.key");            return "ok";        }    });}RetryStatistics statistics = repository.findOne("method.key");System.out.println(statistics);

  此處要給操作定義一個name如“method.key”,從而查詢該操作的統計分析資料。 
  到此spring-retry重試與熔斷就介紹完了。spring-retry專案地址https://github.com/spring-projects/spring-retry。 
  另外可以參考《億級流量網站架構核心技術》的《第5章 降級特技》和《第6章 超時與重試機制》瞭解和學習更多內容。
                     圖片描述

           

給我老師的人工智慧教程打call!http://blog.csdn.net/jiangjunshow

這裡寫圖片描述