再談優雅重試(retry)機制

業務場景

應用中須要實現一個功能: 須要將數據上傳到遠程存儲服務,同時在返回處理成功狀況下作其餘操做。這個功能不復雜,分爲兩個步驟:第一步調用遠程的Rest服務邏輯包裝給處理方法返回處理結果;第二步拿到第一步結果或者捕捉異常,若是出現錯誤或異常實現重試上傳邏輯,不然繼續邏輯操做。java

解決方案演化

這個問題的技術點在於可以觸發重試,以及重試狀況下邏輯有效執行。程序員

解決方案一:try-catch-redo簡單重試模式

包裝正常上傳邏輯基礎上,經過判斷返回結果或監聽異常決策是否重試,同時爲了解決當即重試的無效執行(假設異常是有外部執行不穩定致使的),休眠必定延遲時間從新執行功能邏輯。算法

public void commonRetry(Map<String, Object> dataMap) throws InterruptedException {
		Map<String, Object> paramMap = Maps.newHashMap();
		paramMap.put("tableName", "creativeTable");
		paramMap.put("ds", "20160220");
		paramMap.put("dataMap", dataMap);
		boolean result = false;
		try {
			result = uploadToOdps(paramMap);
			if (!result) {
				Thread.sleep(1000);
				uploadToOdps(paramMap);  //一次重試
			}
		} catch (Exception e) {
			Thread.sleep(1000);
			uploadToOdps(paramMap);//一次重試
		}
	}
複製代碼

解決方案二:try-catch-redo-retry strategy策略重試模式

上述方案仍是有可能重試無效,解決這個問題嘗試增長重試次數retrycount以及重試間隔週期interval,達到增長重試有效的可能性。spring

public void commonRetry(Map<String, Object> dataMap) throws InterruptedException {
		Map<String, Object> paramMap = Maps.newHashMap();
		paramMap.put("tableName", "creativeTable");
		paramMap.put("ds", "20160220");
		paramMap.put("dataMap", dataMap);
		boolean result = false;
		try {
			result = uploadToOdps(paramMap);
			if (!result) {
				reuploadToOdps(paramMap,1000L,10);//延遲屢次重試
			}
		} catch (Exception e) {
			reuploadToOdps(paramMap,1000L,10);//延遲屢次重試
		}
	}
複製代碼

方案一和方案二存在一個問題:正常邏輯和重試邏輯強耦合,重試邏輯很是依賴正常邏輯的執行結果,對正常邏輯預期結果被動重試觸發,對於重試根源每每因爲邏輯複雜被淹沒,可能致使後續運維對於重試邏輯要解決什麼問題產生不一致理解。重試正確性難保證並且不利於運維,緣由是重試設計依賴正常邏輯異常或重試根源的臆測。設計模式

優雅重試方案嘗試

那有沒有能夠參考的方案實現正常邏輯和重試邏輯解耦,同時可以讓重試邏輯有一個標準化的解決思路?答案是有:那就是基於代理設計模式的重試工具,咱們嘗試使用相應工具來重構上述場景。安全

嘗試方案一:應用命令設計模式解耦正常和重試邏輯

命令設計模式具體定義不展開闡述,主要該方案看中命令模式可以經過執行對象完成接口操做邏輯,同時內部封裝處理重試邏輯,不暴露實現細節,對於調用者來看就是執行了正常邏輯,達到解耦的目標,具體看下功能實現。(類圖結構)bash

IRetry約定了上傳和重試接口,其實現類OdpsRetry封裝ODPS上傳邏輯,同時封裝重試機制和重試策略。與此同時使用recover方法在結束執行作恢復操做。服務器

而咱們的調用者LogicClient無需關注重試,經過重試者Retryer實現約定接口功能,同時 Retryer須要對重試邏輯作出響應和處理, Retryer具體重試處理又交給真正的IRtry接口的實現類OdpsRetry完成。經過採用命令模式,優雅實現正常邏輯和重試邏輯分離,同時經過構建重試者角色,實現正常邏輯和重試邏輯的分離,讓重試有更好的擴展性。併發

嘗試方案二:spring-retry 規範正常和重試邏輯

spring-retry是一個開源工具包,目前可用的版本爲1.1.2.RELEASE,該工具把重試操做模板定製化,能夠設置重試策略和回退策略。同時重試執行實例保證線程安全,具體場景操做實例以下:app

public void upload(final Map<String, Object> map) throws Exception {
		// 構建重試模板實例
		RetryTemplate retryTemplate = new RetryTemplate();
		// 設置重試策略,主要設置重試次數
		SimpleRetryPolicy policy = new SimpleRetryPolicy(3, Collections.<Class<? extends Throwable>, Boolean> singletonMap(Exception.class, true));
		// 設置重試回退操做策略,主要設置重試間隔時間
		FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
		fixedBackOffPolicy.setBackOffPeriod(100);
		retryTemplate.setRetryPolicy(policy);
		retryTemplate.setBackOffPolicy(fixedBackOffPolicy);
		// 經過RetryCallback 重試回調實例包裝正常邏輯邏輯,第一次執行和重試執行執行的都是這段邏輯
		final RetryCallback<Object, Exception> retryCallback = new RetryCallback<Object, Exception>() {
			//RetryContext 重試操做上下文約定,統一spring-try包裝 
			public Object doWithRetry(RetryContext context) throws Exception {
				System.out.println("do some thing");
				Exception e = uploadToOdps(map);
				System.out.println(context.getRetryCount());
				throw e;//這個點特別注意,重試的根源經過Exception返回
			}
		};
		// 經過RecoveryCallback 重試流程正常結束或者達到重試上限後的退出恢復操做實例
		final RecoveryCallback<Object> recoveryCallback = new RecoveryCallback<Object>() {
			public Object recover(RetryContext context) throws Exception {
				System.out.println("do recory operation");
				return null;
			}
		};
		try {
			// 由retryTemplate 執行execute方法開始邏輯執行
			retryTemplate.execute(retryCallback, recoveryCallback);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
複製代碼

簡單剖析下案例代碼,RetryTemplate 承擔了重試執行者的角色,它能夠設置SimpleRetryPolicy(重試策略,設置重試上限,重試的根源實體),FixedBackOffPolicy(固定的回退策略,設置執行重試回退的時間間隔)。 RetryTemplate經過execute提交執行操做,須要準備RetryCallback 和RecoveryCallback 兩個類實例,前者對應的就是重試回調邏輯實例,包裝正常的功能操做,RecoveryCallback實現的是整個執行操做結束的恢復操做實例。

RetryTemplate的execute 是線程安全的,實現邏輯使用ThreadLocal保存每一個執行實例的RetryContext執行上下文。

Spring-retry工具雖能優雅實現重試,可是存在兩個不友好設計:一個是 重試實體限定爲Throwable子類,說明重試針對的是可捕捉的功能異常爲設計前提的,可是咱們但願依賴某個數據對象實體做爲重試實體,但Sping-retry框架必須強制轉換爲Throwable子類。另外一個就是重試根源的斷言對象使用的是doWithRetry的Exception 異常實例,不符合正常內部斷言的返回設計。

Spring Retry提倡以註解的方式對方法進行重試,重試邏輯是同步執行的,重試的「失敗」針對的是Throwable,若是你要以返回值的某個狀態來斷定是否須要重試,可能只能經過本身判斷返回值而後顯式拋出異常了。

Spring 對於Retry的抽象

「抽象」是每一個程序員必備的素質。對於資質平平的我來講,沒有比模仿與理解優秀源碼更好地進步途徑了吧。爲此,我將其核心邏輯重寫了一遍…下面就看看Spring Retry對於「重試」的抽象。

「重試」邏輯

while(someCondition()) {
    try{
        doSth();
        break;    
    } catch(Throwable th) {
        modifyCondition();
        wait();
    }
}
if(stillFail) {
    doSthWhenStillFail();
}複製代碼

同步重試代碼基本能夠表示爲上述,可是Spring Retry對其進行了很是優雅地抽象,雖然主要邏輯不變,可是看起來倒是舒服多了。主要的接口抽象以下圖所示:

Spring retry相關接口.jpg
  • RetryCallback: 封裝你須要重試的業務邏輯(上文中的doSth)
  • RecoverCallback:封裝在屢次重試都失敗後你須要執行的業務邏輯(上文中的doSthWhenStillFail)
  • RetryContext: 重試語境下的上下文,可用於在屢次Retry或者Retry 和Recover之間傳遞參數或狀態(在屢次doSth或者doSth與doSthWhenStillFail之間傳遞參數)
  • RetryOperations : 定義了「重試」的基本框架(模板),要求傳入RetryCallback,可選傳入RecoveryCallback;
  • RetryListener:典型的「監聽者」,在重試的不一樣階段通知「監聽者」(例如doSth,wait等階段時通知)
  • RetryPolicy : 重試的策略或條件,能夠簡單的進行屢次重試,能夠是指定超時時間進行重試(上文中的someCondition)
  • BackOffPolicy: 重試的回退策略,在業務邏輯執行發生異常時。若是須要重試,咱們可能須要等一段時間(可能服務器過於繁忙,若是一直不間隔重試可能拖垮服務器),固然這段時間能夠是0,也能夠是固定的,能夠是隨機的(參見tcp的擁塞控制算法中的回退策略)。回退策略在上文中體現爲wait();
  • RetryTemplate :RetryOperations的具體實現,組合了RetryListener[],BackOffPolicy,RetryPolicy。

嘗試方案三:guava-retryer 分離正常和重試邏輯

Guava retryer工具與spring-retry相似,都是經過定義重試者角色來包裝正常邏輯重試,可是Guava retryer有更優的策略定義,在支持重試次數和重試頻度控制基礎上,可以兼容支持多個異常或者自定義實體對象的重試源定義,讓重試功能有更多的靈活性。Guava Retryer也是線程安全的,入口調用邏輯採用的是Java.util.concurrent.Callable的call方法,示例代碼以下:

public void uploadOdps(final Map<String, Object> map) {
		// RetryerBuilder 構建重試實例 retryer,能夠設置重試源且能夠支持多個重試源,能夠配置重試次數或重試超時時間,以及能夠配置等待時間間隔
		Retryer<Boolean> retryer = RetryerBuilder.<Boolean> newBuilder()
				.retryIfException().//設置異常重試源
				retryIfResult(new Predicate<Boolean>() {//設置自定義段元重試源,
			@Override
			public boolean apply(Boolean state) {//特別注意:這個apply返回true說明須要重試,與操做邏輯的語義要區分
				return true;
			}
		})
		.withStopStrategy(StopStrategies.stopAfterAttempt(5))//設置重試5次,一樣能夠設置重試超時時間
		.withWaitStrategy(WaitStrategies.fixedWait(100L, TimeUnit.MILLISECONDS)).build();//設置每次重試間隔

		try {
			//重試入口採用call方法,用的是java.util.concurrent.Callable<V>的call方法,因此執行是線程安全的
			boolean result = retryer.call(new Callable<Boolean>() { 
				@Override
				public Boolean call() throws Exception {
					try {
						//特別注意:返回false說明無需重試,返回true說明須要繼續重試
						return uploadToOdps(map);
					} catch (Exception e) {
						throw new Exception(e);
					}
				}
			});

		} catch (ExecutionException e) {
		} catch (RetryException ex) {
		}
	}
複製代碼

示例代碼原理分析:

RetryerBuilder是一個factory建立者,能夠定製設置重試源且能夠支持多個重試源,能夠配置重試次數或重試超時時間,以及能夠配置等待時間間隔,建立重試者Retryer實例。

RetryerBuilder的重試源支持Exception異常對象 和自定義斷言對象,經過retryIfException 和retryIfResult設置,同時支持多個且能兼容。

RetryerBuilder的等待時間和重試限制配置採用不一樣的策略類實現,同時對於等待時間特徵能夠支持無間隔和固定間隔方式。

Retryer 是重試者實例,經過call方法執行操做邏輯,同時封裝重試源操做。

優雅重試共性和原理

  1. 正常和重試優雅解耦,重試斷言條件實例或邏輯異常實例是二者溝通的媒介。
  2. 約定重試間隔,差別性重試策略,設置重試超時時間,進一步保證重試有效性以及重試流程穩定性。
  3. 都使用了命令設計模式,經過委託重試對象完成相應的邏輯操做,同時內部封裝實現重試邏輯。
  4. Spring-tryer和guava-tryer工具都是線程安全的重試,可以支持併發業務場景的重試邏輯正確性。

優雅重試適用場景

  1. 功能邏輯中存在不穩定依賴場景,須要使用重試獲取預期結果或者嘗試從新執行邏輯不當即結束。好比遠程接口訪問,數據加載訪問,數據上傳校驗等等。
  2. 對於異常場景存在須要重試場景,同時但願把正常邏輯和重試邏輯解耦。
  3. 對於須要基於數據媒介交互,但願經過重試輪詢檢測執行邏輯場景也能夠考慮重試方案。

參考資料

https://blog.csdn.net/paul_wei2008/article/details/53871442

相關文章
相關標籤/搜索