個人看法: spring-retry更好
。
軟硬件環境: IntelliJ IDEA、SpringBoot2.2.4.RELEASE。
Spring的Retry組件:
提示: spring-retry的使用方式可分爲註解式和編碼式,註解式採用代理模式依賴於AOP,而編程式則可以直接調用方法。註解式無疑更優雅,但是使用註解式的時候,要注意避免各個AOP執行順序差異帶來的問題,在這個環節的末尾,會簡單介紹如何避免這個問題。本文主要介紹的是註解式用法中基礎的常用的內容;至於spring-retry的編程式用法、spring-retry的註解式用法的其它內容可詳見https://github.com/spring-projects/spring-retry。
準備工作:
-
第一步: 在pom.xml中引入依賴。
<!-- spring-retry --> <dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId> </dependency> <!-- aop支持 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
-
第二步: 在某個配置類(如啓動類)上,啓用@EnableRetry。
Spring Retry的編碼式使用:
提示:編碼式使用spring-retry不是主要內容,這裏就簡單舉個例子就行了。
public Object retryCoding() throws Throwable {
/*
* spring-retry1.3.x版本開始提供建造者模式支持了,可
* 詳見https://github.com/spring-projects/spring-retry
*/
RetryTemplate template = new RetryTemplate();
// 設置重試策略
SimpleRetryPolicy simpleRetryPolicy = new SimpleRetryPolicy();
simpleRetryPolicy.setMaxAttempts(5);
template.setRetryPolicy(simpleRetryPolicy);
// 執行
Object result = template.execute(
new RetryCallback<Object, Throwable>() {
@Override
public Object doWithRetry(RetryContext context) throws Throwable {
// 第一次請求,不算重試, 所以第一次請求時,context.getRetryCount()值爲0
throw new RuntimeException("第" + (context.getRetryCount() + 1) + "次調用失敗!");
}
},
new RecoveryCallback<Object>() {
@Override
public Object recover(RetryContext context) throws Exception {
Throwable lastThrowable = context.getLastThrowable();
return "走recover邏輯了! \t異常類是" + lastThrowable.getClass().getName()
+ "\t異常信息是" + lastThrowable.getMessage();
}
});
System.out.println(result);
return result;
}
注:1.3.x開始,spring-retry提供建造者模式支持RetryTemplate的創建了。
Spring Retry的註解式使用:
- @Retryable默認項: 默認最多請求3次,默認重試時延遲1000ms再進行請求。
- 注:重試兩次, 加上本身那一次一起3次。
- 注:默認在所有異常的情況下,都進行重試;若重試的這幾次都沒有成功,都出現了異常,那麼最終拋出的是最後一次重試時出現的異常。
- 示例:
- 被調用的方法:
private int times = 0; /** * - 默認最多請求3次(注: 重試兩次, 加上本身那一次一起3次) * * - 默認在所有異常的情況下,都進行重試; 若重試的這幾次都沒有成功,都出現了異常, * 那麼最終拋出的是最後一次重試時出現的異常 */ @Retryable public String methodOne() { times++; int i = ThreadLocalRandom.current().nextInt(10); if (i < 9) { if (times == 3) { throw new IllegalArgumentException("最後一次重試時, 發生了IllegalArgumentException異常"); } throw new RuntimeException("times=" + times + ", 當前i的值爲" + i); } return "在第【" + times + "】次調用時, 調通了!"; }
- 測試方法:
- 程序輸出:
- 被調用的方法:
- @Retryable的include與exclude: 默認最多請求3次,默認重試時延遲1000ms再進行請求。
- 在嘗試次數內:
- 情況一:如果拋出的是include裏面的異常(或其子類異常),那麼仍然會繼續重試。
- 情況二:如果拋出的是include範圍外的異常(或其子類異常) 或者 拋出的是exclude裏面的異常(或其子類異常), 那麼不再繼續重試,直接拋出異常。
注:若拋出的異常即是include裏指定的異常的子類,又是exclude裏指定的異常的子類,那麼判斷當前異常是按include走,還是按exclude走,需要根據【更短路徑原則】。如下面的methodTwo方法所示, RuntimeException 是 IllegalArgumentException的超類,IllegalArgumentException 又是 NumberFormatException的超類,此時因爲IllegalArgumentException離NumberFormatException“路徑更短”,所以拋出的NumberFormatException按照IllegalArgumentException算,走include。
- 示例:
- 被調用的方法:
private int times = 0; /** * - 在嘗試次數內, * 1. 如果拋出的是include裏面的異常(或其子類異常),那麼仍然會繼續重試 * 2. 如果拋出的是include範圍外的異常(或其子類異常) 或者 拋出的是 * exclude裏面的異常(或其子類異常), 那麼不再繼續重試,直接拋出異常 * * 注意: 若拋出的異常即是include裏指定的異常的子類,又是exclude裏指定的異常的子類,那麼 * 判斷當前異常是按include走,還是按exclude走,需要根據【更短路徑原則】。 * 如本例所示, RuntimeException 是 IllegalArgumentException的超類, * IllegalArgumentException 又是 NumberFormatException的超類, * 此時因爲IllegalArgumentException離NumberFormatException“路徑更短”, * 所以拋出的NumberFormatException按照IllegalArgumentException算,走include。 */ @Retryable(include = {IllegalArgumentException.class}, exclude = {RuntimeException.class}) public String methodTwo() { times++; /// if (times == 1) { /// throw new IllegalArgumentException("times=" + times + ", 發生的異常是IllegalArgumentException"); /// } /// if (times == 2) { /// throw new RuntimeException("times=" + times + ", 發生的異常是RuntimeException"); /// } if (times == 1) { throw new NumberFormatException("times=" + times + ", 發生的異常是IllegalArgumentException的子類"); } if (times == 2) { throw new ArithmeticException("times=" + times + ", 發生的異常是RuntimeException的子類"); } return "在第【" + times + "】次調用時, 調通了!"; } /** * - 在嘗試次數內, * 如果拋出的是exclude裏面的異常(或其子類異常),那麼不再繼續重試,直接拋出異常 * 如果拋出的是include裏面的異常(或其子類異常),那麼仍然會繼續重試 */ @Retryable(include = {RuntimeException.class}, exclude = {IllegalArgumentException.class}) public String methodTwoAlpha() { times++; if (times == 1) { throw new ArithmeticException("times=" + times + ", 發生的異常是RuntimeException的子類"); } if (times == 2) { throw new NumberFormatException("times=" + times + ", 發生的異常是IllegalArgumentException的子類"); } return "在第【" + times + "】次調用時, 調通了!"; } /** * - 在嘗試次數內, * 如果拋出的是include範圍外的異常(或其子類異常),那麼不再繼續重試,直接拋出異常 * 如果拋出的是include裏面的異常(或其子類異常),那麼仍然會繼續重試 */ @Retryable(include = {IllegalArgumentException.class}) public String methodTwoBeta() { times++; if (times == 1) { throw new NumberFormatException("times=" + times + ", 發生的異常是IllegalArgumentException的子類"); } if (times == 2) { throw new ArithmeticException("times=" + times + ", 發生的異常是RuntimeException的子類"); } return "在第【" + times + "】次調用時, 調通了!"; }
- 測試方法:
- 三個測試方法對應的輸出:
- 被調用的方法:
- 在嘗試次數內:
- @Retryable的maxAttempts: maxAttempts用於指定最大嘗試次數, 默認值爲3。
- 連本身那一次也會被算在內(若值爲5, 那麼最多重試4次, 算上本身那一次5次)。
- 示例:
- 被調用的方法:
private int times = 0; /** * maxAttempts指定最大嘗試次數, 默認值爲3. * 注:連本身那一次也會被算在內(若值爲5, 那麼最多重試4次, 算上本身那一次5次) */ @Retryable(maxAttempts = 5) public String methodThere() { times++; if (times < 5) { throw new RuntimeException("times=" + times + ", 發生的異常是RuntimeException"); } return "在第【" + times + "】次調用時, 調通了!"; }
- 測試方法:
- 程序輸出:
- 被調用的方法:
- @Retryable與@Recover搭配:
- 相關要點一: 我們不妨稱被@Retryable標記的方法爲目標方法,稱被@Recover標記的方法爲處理方法。那麼處理方法和目標方法必須同時滿足:
- 處於同一個類下。
- 兩者的參數類型需要匹配 或 處理方法的參數可以多一個異常接收類(這一異常接收類必須放在第一個參數的位置)。
注:兩者的參數類型匹配即可,形參名可以一樣可以不一樣。 - 返回值類型需要保持一致(或處理方法的返回值類型是目標方法的返回值類型的超類)。
- 相關要點二: 目標方法在進行完畢retry後,如果仍然拋出異常, 那麼會去定位處理方法, 走處理方法的邏輯,定位處理方法的原則是:在同一個類下,尋找和目標方法 具有相同參數類型(P.S.可能會再參數列表首位多一個異常類參數)、相同返回值類型的標記有Recover的方法。
注:如果存在兩個目標方法,他們的參數類型、返回值類型都一樣,這時就需要主動指定對應的處理方法了,如:@Retryable(recover = “service1Recover”)。@Retryable註解的recover 屬性,在spring-retry1.3.x版本纔開始提供。
注:如果是使用的1.3.x+版本的spring-retry
,推薦直接使用@Retryable(recover = "recoverMethodName")指定同類當中的處理方法的方法名
。 - 示例:
- 被調用的方法:
import org.springframework.retry.annotation.Recover; import org.springframework.retry.annotation.Retryable; import org.springframework.stereotype.Component; /** * 目標方法:被@Retryable標記的方法 * 處理方法:被@Recover標記的方法 * * 處理方法 和 目標方法 必須滿足: * 1. 處於同一個類下 * 2. 兩者的參數需要保持一致 或 處理方法的參數可以多一個異常接收類(這一異常接收類必須放在第一個參數的位置) * 注:保持一致指的是參數類型保持一致,形參名可以一樣可以不一樣 * 3. 返回值類型需要保持一致 (或處理方法的返回值類型是目標方法的返回值類型的超類 ) * * 目標方法在進行完畢retry後,如果仍然拋出異常, 那麼會去定位處理方法, 走處理方法的邏輯,定位處理方法的原則是: * - 在同一個類下,尋找和目標方法 具有 * 相同參數類型(P.S.可能會再參數列表首位多一個異常類參數)、 * 相同返回值類型 * 的標記有Recover的方法 * - 如果存在兩個目標方法,他們的參數類型、返回值類型都一樣, * 這時就需要主動指定對應的處理方法了, * 如:@Retryable(recover = "service1Recover") * * @author JustryDeng * @date 2020/2/25 21:40:11 */ @Component public class QwerRemoteCall { private int times = 0; /// --------------------------------------------------------- @Recover基本測試 @Retryable public String methodFour(Integer a, String b) { times++; throw new RuntimeException("times=" + times + ", 發生的異常是RuntimeException"); } @Recover private String justryDeng(Throwable th, Integer a, String b) { return "a=" + a + ", b=" + b + "\t" + "異常類是:" + th.getClass().getName() + ", 異常信息是:" + th.getMessage(); } /// 如果在@Retryable中指明瞭異常, 那麼在@Recover中可以明確的指明是哪一種異常 /// @Retryable(RemoteAccessException.class) /// public void service() { /// // ... do something /// } /// /// @Recover /// public void recover(RemoteAccessException e) { /// // ... panic /// } /// --------------------------------------------------------- @Retryable指定對應的@Recover方法 /// 特別注意: @Retryable註解的recover屬性, 在spring-retry的較高版本中才得以支持, /// 在本人使用的1.2.5.RELEASE版本中還暫不支持 /// @Retryable(recover = "service1Recover", value = RemoteAccessException.class) /// public void service1(String str1, String str2) { /// // ... do something /// } /// /// @Retryable(recover = "service2Recover", value = RemoteAccessException.class) /// public void service2(String str1, String str2) { /// // ... do something /// } /// /// @Recover /// public void service1Recover(RemoteAccessException e, String str1, String str2) { /// // ... error handling making use of original args if required /// } /// /// @Recover /// public void service2Recover(RemoteAccessException e, String str1, String str2) { /// // ... error handling making use of original args if required /// } }
- 測試方法:
- 程序輸出:
- 被調用的方法:
- 相關要點一: 我們不妨稱被@Retryable標記的方法爲目標方法,稱被@Recover標記的方法爲處理方法。那麼處理方法和目標方法必須同時滿足:
- @Retryable的backoff: @Retryable註解的backoff屬性,可用於指定重試時的退避策略。
- 相關要點:
- @Retryable 或 @Retryable(backoff = @Backoff()), 那麼默認延遲 1000ms
後重試。 - @Backoff的delay屬性: 延遲多久後,再進行重試。
- 如果不想延遲, 那麼需要指定@Backoff的value和delay同時爲0。
- delay與multiplier搭配使用,
延遲時間 = delay * (multiplier ^ (n - 1))
,其中n爲第幾次重試, n >= 1, 這裏^爲次方。
注:第二次請求,纔算第一次重試。
- @Retryable 或 @Retryable(backoff = @Backoff()), 那麼默認延遲 1000ms
- 示例:
- 被調用的方法:
private int times = 0; DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss"); /** * Backoff用於指定 重試時的退避策略 * - @Retryable 或 @Retryable(backoff = @Backoff()), 那麼默認延遲 1000ms後重試 * 注:第一次請求時,是馬上進行的,是不會延遲的 * * 效果如: * times=1, 時間是12:02:04 * times=2, 時間是12:02:05 * times=3, 時間是12:02:06 */ @Retryable(backoff = @Backoff()) public String methodFive() { times++; System.err.println("times=" + times + ", 時間是" + dateTimeFormatter.format(LocalTime.now())); throw new RuntimeException("times=" + times + ", 發生的異常是RuntimeException"); } /** * - delay: 延遲多久後,再進行重試。 * 注:第一次請求時,是馬上進行的,是不會延遲的 * * 效果如: * times=1, 時間是11:46:36 * times=2, 時間是11:46:41 * times=3, 時間是11:46:46 */ @Retryable(backoff = @Backoff(delay = 5000)) public String methodFiveAlpha() { times++; System.err.println("times=" + times + ", 時間是" + dateTimeFormatter.format(LocalTime.now())); throw new RuntimeException("times=" + times + ", 發生的異常是RuntimeException"); } /** * 如果不想延遲, 那麼需要指定value和delay同時爲0 * 注:原因可詳見javadoc 或 源碼 * * 效果如: * times=1, 時間是12:05:44 * times=2, 時間是12:05:44 * times=3, 時間是12:05:44 */ @Retryable(backoff = @Backoff(value = 0, delay = 0)) public String methodFiveBeta() { times++; System.err.println("times=" + times + ", 時間是" + dateTimeFormatter.format(LocalTime.now())); throw new RuntimeException("times=" + times + ", 發生的異常是RuntimeException"); } /** * - delay: 延遲多久後,再進行重試。 * - multiplier: 乘數因子 * * 延遲時間 = delay * (multiplier ^ (n - 1)) , 其中n爲第幾次重試, n >= 1, 這裏 ^ 爲次方 * * 注:第一次請求時,是馬上進行的,是不會延遲的 * 注:第二次請求時對應第一次重試 * * 效果如: * times=1, 時間是12:09:14 * times=2, 時間是12:09:17 * times=3, 時間是12:09:23 * times=4, 時間是12:09:35 * times=5, 時間是12:09:59 * 可知,延遲時間越來越大,分別是: 3 6 12 24 */ @Retryable(maxAttempts = 5, backoff = @Backoff(delay = 3000, multiplier = 2)) public String methodFiveGamma() { times++; System.err.println("times=" + times + ", 時間是" + dateTimeFormatter.format(LocalTime.now())); throw new RuntimeException("times=" + times + ", 發生的異常是RuntimeException"); }
- 測試方法:
- 四個測試方法分別輸出:
- 被調用的方法:
- 相關要點:
使用spring retry註解式時,避免多個AOP代理導致可能出現的問題:
-
情景說明: 就像@Transactional與@CacheEvict標註在同一個方法上、@Transactional與synchronized標註在同一個方法上一樣,在併發情況下,會出現問題(會出現什麼問題、怎麼解決出現的問題可詳見《程序員成長筆記(第二部)》相關章節)。如果@Transactional和@Retryable同時標註在了同一個方法上,那是不是也會出問題呢,從原理分析,肯定是會出現問題的,如下面的錯誤示例。
-
錯誤示例:
- 某個service實現如圖:
- 調用一次該方法前的表:
- 調用一次該方法後的表:
這裏只是拿事務AOP與重試AOP舉的一個例子,重點是說,在多個AOP同時作用於同一個方法時,應該考慮各個AOP之間的執行順序問題;更好的辦法是儘量避免多個AOP作用於同一個切點。
- 某個service實現如圖:
-
正確示例(避免方式):
將重試機制那部分代碼,單獨放在一個類裏面,避免多個AOP作用於同一個切點
。
這個時候,哪怕仍然通過@EnableTransactionManagement(order = Ordered.HIGHEST_PRECEDENCE)把事務的AOP優先級調到了最高,也不會有什麼影響了,也不會出現上面錯誤示例中多條數據的問題了。
注:避免方式較多(如主動控制各個AOP直接的執行順序、避免多個AOP作用於同一個切點等),推薦使用避免多個AOP作用於同一個切點。
Guava的Retry組件:
準備工作:在pom.xml中引入依賴。
<!-- guava retry -->
<dependency>
<groupId>com.github.rholder</groupId>
<artifactId>guava-retrying</artifactId>
<version>2.0.0</version>
</dependency>
Guava Retry的使用:
比起Spring Retry的使用, Guava Retry的使用方式相對簡單,這裏僅給出一個簡單的使用示例,更多細節可詳見https://github.com/rholder/guava-retrying。
簡單使用示例:
import com.github.rholder.retry.RetryException;
import com.github.rholder.retry.Retryer;
import com.github.rholder.retry.RetryerBuilder;
import com.github.rholder.retry.StopStrategies;
import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ThreadLocalRandom;
import java.util.zip.DataFormatException;
/**
* Guava Retry簡單使用示例
*
* @author JustryDeng
* @date 2020/2/25 21:40:11
*/
public class XyzRemoteCall {
/**
* guava retry組件 使用測試
*
* 提示:泛型 對應 要返回的數據的類型。
*/
public static void jd() {
// 創建callable, 在call()方法裏面編寫相關業務邏輯
Callable<Object[]> callable = new Callable<Object[]>() {
int times = 0;
@Override
public Object[] call() throws Exception {
// business logic
times++;
if (times == 1) {
throw new RuntimeException();
}
if (times == 2) {
throw new Exception();
}
// 隨機一個數[origin, bound)
int randomNum = ThreadLocalRandom.current().nextInt(1, 5);
if (randomNum == 1) {
throw new DataFormatException("call()拋出了檢查異常DataFormatException");
} else if (randomNum == 2) {
throw new IOException("call()拋出了檢查異常IOException");
} else if (randomNum == 3) {
throw new RuntimeException("call()拋出了運行時異常RuntimeException");
}
return new Object[]{"鄧沙利文", "亨得帥", "鄧二洋", "JustryDeng"};
}
};
// 創建重試器
Retryer<Object[]> retryer = RetryerBuilder.<Object[]>newBuilder()
/*
* 指定什麼條件下觸發重試
*
* 注:這裏,只要callable中的call方法拋出的異常是Throwable或者
* 是Throwable的子類,那麼這裏都成立,都會進行重試。
*/
.retryIfExceptionOfType(Throwable.class)
/// .retryIfException()
/// .retryIfRuntimeException()
/// .retryIfExceptionOfType(@Nonnull Class<? extends Throwable> exceptionClass)
/// .retryIfException(@Nonnull Predicate<Throwable> exceptionPredicate)
/// .retryIfResult(@Nonnull Predicate<V> resultPredicate)
// 設置兩次重試之間的阻塞策略(如: 設置線程sleep、設置自旋鎖等等)
///.withBlockStrategy()
// 設置監聽器 (這個監聽器可用於監聽每次請求的結果信息, 並作相應的邏輯處理。 如: 統計、預警等等)
///.withRetryListener()
// 設置延時策略, 每次重試前,都要延時一段時間,然後再發起請求。(第一次請求,是不會被延時的)
///.withWaitStrategy()
// 設置停止重試的策略(如:這裏設置的是三次請求後, 不再重試)
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
.build();
try {
Object[] result = retryer.call(callable);
System.err.println(Arrays.toString(result));
/*
* call()方法拋出的異常會被封裝到RetryException或ExecutionException中, 進行拋出
* 所以在這裏,可以通過 e.getCause()獲取到call()方法實際拋出的異常
*/
} catch (RetryException|ExecutionException e) {
System.err.println("call()方法拋出的異常, 實際是" + e.getCause());
e.printStackTrace();
}
}
}
Spring Retry重試組件、Guava Retry重試組件簡單梳理完畢 !
^_^ 如有不當之處,歡迎指正
^_^ 參考連接
https://github.com/spring-projects/spring-retry
https://github.com/rholder/guava-retrying
^_^ 測試代碼託管連接
https://github.com/JustryDeng/CommonRepository…
^_^ 本文已經被收錄進《程序員成長筆記(七)》,筆者JustryDeng