初探SpringRetry機制

重試是在網絡通訊中非常重要的概念,尤其是在微服務體系內重試顯得格外重要。常見的場景是當遇到網絡抖動造成的請求失敗時,可以按照業務的補償需求來制定重試策略。Spring框架提供了SpringRetry能讓我們在項目工程中很方便的使用重試。這裡我主要試着分析一下在Spring框架的各個核心模塊里如何集成是使用SpringRetry。

1. SpringRetry的快速上手示例

首先我們引入springRetry的依賴: implementation group: 'org.springframework.retry', name: 'spring-retry',version:'1.2.5.RELEASE',同時已SpringBoot作為項目示例基礎:

    @Bean
    public RetryTemplate retryTemplate() {
        RetryTemplate retryTemplate = new RetryTemplate();
        TimeoutRetryPolicy timeoutRetryPolicy = new TimeoutRetryPolicy(); //定義基於時間超時的重試策略
        timeoutRetryPolicy.setTimeout(3000L);
        retryTemplate.setRetryPolicy(timeoutRetryPolicy);
        return retryTemplate;

    }


   public static void main(String[]args){
     
        ConfigurableApplicationContext applicationContext = SpringApplication.run(RetryDemo.class, args);
        RetryTemplate retryTemplate = applicationContext.getBean(RetryTemplate.class);
        retryTemplate.execute(new RetryCallback<Object, Throwable>() { //2 重試執行的代碼塊
            private int index = 1;

            @Override
            public Object doWithRetry(RetryContext context) throws Throwable {
                System.out.println("第" + index + "次執行");
                index++;
                System.out.println(1 / 0);//3  重試代碼塊里模擬異常的操作
                return null;
            }
        });
     
   }

上面的代碼示例展示了SpringRetry的最基本使用方式,首先在代碼1處先定義了RetryTemplate的Bean,同時定義了重試的策略配置,等會說一下這個RetryPolicy接口。在此處定義TimeoutRetryPolicy主要控制重試多長時間,此處設置3秒。3秒後沒有成功的話結束重試。在代碼2處,使用RetryTemplate的execute方法來執行重試的回調,如果在回調中出現異常則執行重試,因此我們可以把執行重試的代碼塊放在這裡。在代碼3處模擬了拋出異常的操作

這裡運行的結果:

....
第110823次執行
第110824次執行
第110825次執行
第110826次執行
Exception in thread "main" java.lang.ArithmeticException: / by zero

2. SpringRetry的核心代碼

RetryTemplate里核心方法時exectute方法,這裡可以從這個入口方法來了解代碼的核心。

  1. 初始化RetryContext
	// Allow the retry policy to initialise itself...
		RetryContext context = open(retryPolicy, state);
		if (this.logger.isTraceEnabled()) {
			this.logger.trace("RetryContext retrieved: " + context);
		}

在這裡會調用給RetryPolicy接口的open方法,目的是為了初始化RetryContext(重試上下文),RetryContext可以擴展我們的業務字段,這裡我舉個例子TimeoutRetryContext中定義了timeout與start屬性。LoadBalancedRetryContext中定義了request與serviceInstance

  1. 調用RetryListener的onOpen方法


// Give clients a chance to enhance the context...
			boolean running = doOpenInterceptors(retryCallback, context);


private <T, E extends Throwable> boolean doOpenInterceptors(
			RetryCallback<T, E> callback, RetryContext context) {

		boolean result = true;

		for (RetryListener listener : this.listeners) {
			result = result && listener.open(context, callback);
		}

		return result;

	}

在這裡單行注釋已經明明白白的寫出了,我們可以利用RetryListener的來給客戶端一個機會增強RetryContext的機會,如果理解攔截器的思想的話,這點應該很容易理解。

  1. 獲取和啟動BackOffContext

    // Get or Start the backoff context...
    			BackOffContext backOffContext = null;
    			Object resource = context.getAttribute("backOffContext");
    
    			if (resource instanceof BackOffContext) {
    				backOffContext = (BackOffContext) resource;
    			}
    
    			if (backOffContext == null) {
    				backOffContext = backOffPolicy.start(context);
    				if (backOffContext != null) {
    					context.setAttribute("backOffContext", backOffContext);
    				}
    			}
    

    先從RetryContext中獲取backOffContext,如果沒有會調用backOffPolicy的start方法。該方法會返回BackOffContext對象不等於null的情況下把此對象放在RetryContext當中

  2. 執行RetryCallback的代碼回調

    	/*
    			 * We allow the whole loop to be skipped if the policy or context already
    			 * forbid the first try. This is used in the case of external retry to allow a
    			 * recovery in handleRetryExhausted without the callback processing (which
    			 * would throw an exception).
    			 */
    			while (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) {
    
    				try {
    					if (this.logger.isDebugEnabled()) {
    						this.logger.debug("Retry: count=" + context.getRetryCount());
    					}
    					// Reset the last exception, so if we are successful
    					// the close interceptors will not think we failed...
    					lastException = null;
    					return retryCallback.doWithRetry(context);
    				}
    				catch (Throwable e) {
    
    					lastException = e;
    
    					try {
    						registerThrowable(retryPolicy, state, context, e);
    					}
    					catch (Exception ex) {
    						throw new TerminatedRetryException("Could not register throwable",
    								ex);
    					}
    					finally {
    						doOnErrorInterceptors(retryCallback, context, e);
    					}
    
    					if (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) {
    						try {
    							backOffPolicy.backOff(backOffContext);
    						}
    						catch (BackOffInterruptedException ex) {
    							lastException = e;
    							// back off was prevented by another thread - fail the retry
    							if (this.logger.isDebugEnabled()) {
    								this.logger
    										.debug("Abort retry because interrupted: count="
    												+ context.getRetryCount());
    							}
    							throw ex;
    						}
    					}
    
    					if (this.logger.isDebugEnabled()) {
    						this.logger.debug(
    								"Checking for rethrow: count=" + context.getRetryCount());
    					}
    
    					if (shouldRethrow(retryPolicy, context, state)) {
    						if (this.logger.isDebugEnabled()) {
    							this.logger.debug("Rethrow in retry for policy: count="
    									+ context.getRetryCount());
    						}
    						throw RetryTemplate.<E>wrapIfNecessary(e);
    					}
    
    				}
    
    				/*
    				 * A stateful attempt that can retry may rethrow the exception before now,
    				 * but if we get this far in a stateful retry there's a reason for it,
    				 * like a circuit breaker or a rollback classifier.
    				 */
    				if (state != null && context.hasAttribute(GLOBAL_STATE)) {
    					break;
    				}
    			}
    
    

    此處代碼是重試代碼的執行核心:

    1. 首先是用一個循環執行RetryCallback的回調內容,此處循環條件有兩個:
      • 根據RetryPolicy接口裡的canRetry方法的返回值,比如說TimeoutPolicy 里控制retry的條件是(System.currentTimeMillis() - start) <= timeout
      • 還有一個是RetryContext中的isExhaustedOnly()方法,這個方法是一個「筋疲力盡」的標誌如果為true那麼也會停止重試的循環
    2. 當出現異常時
      • 首先會調用RetryPolicy里的registerThrowable的方法。然後當RetryState不為null時會把RetryContext對象放入RetryContextCache中保存。
      • 依次循環調用RetryListener里的onError方法
      • 再次判斷重試條件,如果滿足的情況下,會調用BackoffPolicy的backoff方法。BackOffPolicy的接口主要用於定義在執行重試時出現異常情況的處理邏輯,舉個例子說明:FixedBackOffPolicy重寫的backoff方法里可以控制線程休眠讓其間隔多長時間進行重試: sleeper.sleep(backOffPeriod)
      • 調用RetryState的rollbackFor方法的方法來判斷當前的異常需不需要回滾,而在該接口的唯一實現類DefaultRetryState里會用Classifier<? super Throwable, Boolean> rollbackClassifier來將Throwable類型轉換成Boolean類型,可以在這裡定義處理邏輯。如果該方法的返回值為true,將會重新拋出異常

3. SpringRetry的註解式配置

使用註解的方式是SpringRetry中最常見的方式,其實它底層還是aop與前面所說的基本執行流程。對於註解的方式使用起來還是比較簡單的。首先我們打上@EnableRetry的註解

@SpringBootApplication
@EnableRetry
public class SpringApplicationProvider {
 	 
  //....
}


    @Retryable(maxAttempts = 2, recover = "recover") 
    public void send() {
        System.out.println("send....");
        System.out.println(1 / 0);
    }

    @Recover
    public void recover(ArithmeticException exception) {
        System.out.println(2 / 1);
    }

可以在需要重試的方法上打上@Retryable ,這個註解有幾個常見的屬性:

  • maxAttepts 這個定義了重試的最大次數,默認值是3
  • recover : 用於定義@Recover修飾的方法名,旨在指定重試失敗後用於執行的方法。@Recover修飾的方法參數必須是Throwable的子類,同時返回值必須和@Retryable修飾的返回值相同
  • stateful:表示重試是有狀態的,默認值為false,如果為true則有如下影響: 1.會重新拋出異常 2.執行重試期間,註冊當前的重試上下文至緩存
  • backoff : 這個屬性用於收集Backoff的元數據,那麼根據文檔描述,可以得到以下信息:
    • 沒有明確設置的情況下,默認值為 1000 毫秒的固定延遲
      只有delay()設置:設置固定的延遲時間進行重試
      當設置delay()和maxDelay() ,延時的值在兩個值之間均勻分佈
      使用delay() 、 maxDelay()和multiplier()後退指數增長到最大值
      如果設置了random()標誌,則從 [1, multiplier-1] 中的均勻分佈中為每個延遲選擇乘數
    • 實際上這個幾個屬性是控制創建BackOff接口實現類的:
      • 如果multiplier大於0,則創建ExponentialBackOffPolicy對象
      • 如果max > min,則創建UniformRandomBackOffPolicy對象
      • 否則創建FixedBackOffPolicy對象
Tags: