消息重試框架 Spring-Retry 和 Guava-Retry,這個框架有點意思
一 重試框架之 Spring-Retry
Spring Retry 爲 Spring 應用程序提供了聲明性重試支持。它用於 Spring 批處理、Spring 集成、Apache Hadoop(等等)。它主要是針對可能拋出異常的一些調用操作,進行有策略的重試
1. Spring-Retry 的普通使用方式
1. 準備工作
我們只需要加上依賴:
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>1.2.2.RELEASE</version>
</dependency>
準備一個任務方法,我這裏是採用一個隨機整數,根據不同的條件返回不同的值,或者拋出異常
package com.zgd.demo.thread.retry;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomUtils;
import org.springframework.remoting.RemoteAccessException;
/**
* @Author: zgd
* @Description:
*/
@Slf4j
public class RetryDemoTask {
/**
* 重試方法
* @return
*/
public static boolean retryTask(String param) {
log.info("收到請求參數:{}",param);
int i = RandomUtils.nextInt(0,11);
log.info("隨機生成的數:{}",i);
if (i == 0) {
log.info("爲0,拋出參數異常.");
throw new IllegalArgumentException("參數異常");
}else if (i == 1){
log.info("爲1,返回true.");
return true;
}else if (i == 2){
log.info("爲2,返回false.");
return false;
}else{
//爲其他
log.info("大於2,拋出自定義異常.");
throw new RemoteAccessException("大於2,拋出遠程訪問異常");
}
}
}
2. 使用 SpringRetryTemplate
這裏可以寫我們的代碼了
package com.zgd.demo.thread.retry.spring;
import com.zgd.demo.thread.retry.RetryDemoTask;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.springframework.remoting.RemoteAccessException;
import org.springframework.retry.backoff.FixedBackOffPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
import java.util.HashMap;
import java.util.Map;
/**
* @Author: zgd
* @Description: spring-retry 重試框架
*/
@Slf4j
public class SpringRetryTemplateTest {
/**
* 重試間隔時間ms,默認1000ms
* */
private long fixedPeriodTime = 1000L;
/**
* 最大重試次數,默認爲3
*/
private int maxRetryTimes = 3;
/**
* 表示哪些異常需要重試,key表示異常的字節碼,value爲true表示需要重試
*/
private Map<Class<? extends Throwable>, Boolean> exceptionMap = new HashMap<>();
@Test
public void test() {
exceptionMap.put(RemoteAccessException.class,true);
// 構建重試模板實例
RetryTemplate retryTemplate = new RetryTemplate();
// 設置重試回退操作策略,主要設置重試間隔時間
FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
backOffPolicy.setBackOffPeriod(fixedPeriodTime);
// 設置重試策略,主要設置重試次數
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(maxRetryTimes, exceptionMap);
retryTemplate.setRetryPolicy(retryPolicy);
retryTemplate.setBackOffPolicy(backOffPolicy);
Boolean execute = retryTemplate.execute(
//RetryCallback
retryContext -> {
boolean b = RetryDemoTask.retryTask("abc");
log.info("調用的結果:{}", b);
return b;
},
retryContext -> {
//RecoveryCallback
log.info("已達到最大重試次數或拋出了不重試的異常~~~");
return false;
}
);
log.info("執行結果:{}",execute);
}
}
簡單剖析下案例代碼,RetryTemplate
承擔了重試執行者的角色,它可以設置SimpleRetryPolicy
(重試策略,設置重試上限,重試的根源實體),FixedBackOffPolicy
(固定的回退策略,設置執行重試回退的時間間隔)。
RetryTemplate
通過execute
提交執行操作,需要準備RetryCallback
和RecoveryCallback
兩個類實例,前者對應的就是重試回調邏輯實例,包裝正常的功能操作,RecoveryCallback
實現的是整個執行操作結束的恢復操作實例.
只有在調用的時候拋出了異常,並且異常是在exceptionMap
中配置的異常,纔會執行重試操作,否則就調用到excute
方法的第二個執行方法RecoveryCallback
中
當然, 重試策略還有很多種, 回退策略也是:
重試策略
-
**NeverRetryPolicy:**只允許調用
RetryCallback
一次,不允許重試 -
**AlwaysRetryPolicy:**允許無限重試,直到成功,此方式邏輯不當會導致死循環
-
**SimpleRetryPolicy:**固定次數重試策略,默認重試最大次數爲 3 次,
RetryTemplate
默認使用的策略 -
**TimeoutRetryPolicy:**超時時間重試策略,默認超時時間爲 1 秒,在指定的超時時間內允許重試
-
**ExceptionClassifierRetryPolicy:**設置不同異常的重試策略,類似組合重試策略,區別在於這裏只區分不同異常的重試
-
**CircuitBreakerRetryPolicy:**有熔斷功能的重試策略,需設置 3 個參數
openTimeout
、resetTimeout
和delegate
-
**CompositeRetryPolicy:**組合重試策略,有兩種組合方式,樂觀組合重試策略是指只要有一個策略允許即可以重試,悲觀組合重試策略是指只要有一個策略不允許即可以重試,但不管哪種組合方式,組合中的每一個策略都會執行
重試回退策略
重試回退策略,指的是每次重試是立即重試還是等待一段時間後重試。
默認情況下是立即重試,如果需要配置等待一段時間後重試則需要指定回退策略BackoffRetryPolicy
。
-
**NoBackOffPolicy:**無退避算法策略,每次重試時立即重試
-
**FixedBackOffPolicy:**固定時間的退避策略,需設置參數
sleeper
和backOffPeriod
,sleeper
指定等待策略,默認是Thread.sleep
,即線程休眠,backOffPeriod
指定休眠時間,默認 1 秒 -
**UniformRandomBackOffPolicy:**隨機時間退避策略,需設置
sleeper
、minBackOffPeriod
和maxBackOffPeriod
,該策略在minBackOffPeriod
,maxBackOffPeriod
之間取一個隨機休眠時間,minBackOffPeriod
默認 500 毫秒,maxBackOffPeriod
默認 1500 毫秒 -
**ExponentialBackOffPolicy:**指數退避策略,需設置參數
sleeper
、initialInterval
、maxInterval
和multiplie
r,initialInterval
指定初始休眠時間,默認 100 毫秒,maxInterval
指定最大休眠時間,默認 30 秒,multiplier
指定乘數,即下一次休眠時間爲當前休眠時間*multiplier
-
**ExponentialRandomBackOffPolicy:**隨機指數退避策略,引入隨機乘數可以實現隨機乘數回退
我們可以根據自己的應用場景和需求,使用不同的策略,不過一般使用默認的就足夠了。
上面的代碼的話, 我簡單的設置了重試間隔爲 1 秒,重試的異常是RemoteAccessException
,下面就是測試代碼的情況: 重試第二次成功的情況:
retryContext
retryContext
2. Spring-Retry 的註解使用方式
既然是 Spring 家族的東西,那麼自然就支持和 Spring-Boot 整合
1. 準備工作
依賴:
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>1.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.1</version>
</dependency>
2. 代碼
在 application 啓動類上加上@EnableRetry
的註解
@EnableRetry
public class Application {
...
}
爲了方便測試,我這裏寫了一個SpringBootTest
的測試基類,需要使用SpringBootTest
的只要繼承這個類就好了
package com.zgd.demo.thread.test;
/**
* @Author: zgd
* @Description:
*/
import com.zgd.demo.thread.Application;
import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
/**
* @Author: zgd
* @Date: 18/09/29 20:33
* @Description:
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
@Slf4j
public class MyBaseTest {
@Before
public void init() {
log.info("----------------測試開始---------------");
}
@After
public void after() {
log.info("----------------測試結束---------------");
}
}
我們只要在需要重試的方法上加@Retryable
,在重試失敗的回調方法上加@Recover
,下面是這些註解的屬性
建一個 service 類
package com.zgd.demo.thread.retry.spring;
import com.zgd.demo.thread.retry.RetryDemoTask;
import com.zgd.demo.thread.test.MyBaseTest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.remoting.RemoteAccessException;
import org.springframework.retry.ExhaustedRetryException;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Component;
/**
* @Author: zgd
* @Description:
*/
@Service
@Slf4j
public class SpringRetryDemo {
/**
* 重試所調用方法
* @param param
* @return
*/
@Retryable(value = {RemoteAccessException.class},maxAttempts = 3,backoff = @Backoff(delay = 2000L,multiplier = 2))
public boolean call(String param){
return RetryDemoTask.retryTask(param);
}
/**
* 達到最大重試次數,或拋出了一個沒有指定進行重試的異常
* recover 機制
* @param e 異常
*/
@Recover
public boolean recover(Exception e,String param) {
log.error("達到最大重試次數,或拋出了一個沒有指定進行重試的異常:",e);
return false;
}
}
然後我們調用這個 service 裏面的 call 方法
package com.zgd.demo.thread.retry.spring;
import com.zgd.demo.thread.test.MyBaseTest;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @Author: zgd
* @Description:
*/
@Component
@Slf4j
public class SpringRetryDemoTest extends MyBaseTest {
@Autowired
private SpringRetryDemo springRetryDemo;
@Test
public void retry(){
boolean abc = springRetryDemo.call("abc");
log.info("--結果是:{}--",abc);
}
}
這裏我依然是RemoteAccessException
的異常才重試,@Backoff(delay = 2000L,multiplier = 2))
表示第一次間隔 2 秒,以後都是次數的 2 倍, 也就是第二次 4 秒,第三次 6 秒.
來測試一下:
遇到了沒有指定重試的異常, 這裏指定重試的異常是@Retryable(value = {RemoteAccessException.class}...
, 所以拋出參數異常IllegalArgumentException
的時候,直接回調 @Recover 的方法
@Recover
的方法
二 重試框架之 Guava-Retry
Guava retryer 工具與 spring-retry 類似,都是通過定義重試者角色來包裝正常邏輯重試,但是 Guava retryer 有更優的策略定義,在支持重試次數和重試頻度控制基礎上,能夠兼容支持多個異常或者自定義實體對象的重試源定義,讓重試功能有更多的靈活性。
Guava Retryer 也是線程安全的,入口調用邏輯採用的是Java.util.concurrent.Callable
的 call 方法,示例代碼如下:
pom.xml 加入依賴
<!-- https://mvnrepository.com/artifact/com.github.rholder/guava-retrying -->
<dependency>
<groupId>com.github.rholder</groupId>
<artifactId>guava-retrying</artifactId>
<version>2.0.0</version>
</dependency>
更改一下測試的任務方法
package com.zgd.demo.thread.retry;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomUtils;
import org.springframework.remoting.RemoteAccessException;
/**
* @Author: zgd
* @Description:
*/
@Slf4j
public class RetryDemoTask {
/**
* 重試方法
* @return
*/
public static boolean retryTask(String param) {
log.info("收到請求參數:{}",param);
int i = RandomUtils.nextInt(0,11);
log.info("隨機生成的數:{}",i);
if (i < 2) {
log.info("爲0,拋出參數異常.");
throw new IllegalArgumentException("參數異常");
}else if (i < 5){
log.info("爲1,返回true.");
return true;
}else if (i < 7){
log.info("爲2,返回false.");
return false;
}else{
//爲其他
log.info("大於2,拋出自定義異常.");
throw new RemoteAccessException("大於2,拋出自定義異常");
}
}
}
Guava
這裏設定跟 Spring-Retry 不一樣,我們可以根據返回的結果來判斷是否重試,比如返回 false 我們就重試
package com.zgd.demo.thread.retry.guava;
import com.github.rholder.retry.*;
import com.zgd.demo.thread.retry.RetryDemoTask;
import org.junit.Test;
import org.springframework.remoting.RemoteAccessException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
/**
* @Author: zgd
* @Description:
*/
public class GuavaRetryTest {
@Test
public void fun01(){
// RetryerBuilder 構建重試實例 retryer,可以設置重試源且可以支持多個重試源,可以配置重試次數或重試超時時間,以及可以配置等待時間間隔
Retryer<Boolean> retryer = RetryerBuilder.<Boolean> newBuilder()
.retryIfExceptionOfType(RemoteAccessException.class)//設置異常重試源
.retryIfResult(res-> res==false) //設置根據結果重試
.withWaitStrategy(WaitStrategies.fixedWait(3, TimeUnit.SECONDS)) //設置等待間隔時間
.withStopStrategy(StopStrategies.stopAfterAttempt(3)) //設置最大重試次數
.build();
try {
retryer.call(() -> RetryDemoTask.retryTask("abc"));
} catch (Exception e) {
e.printStackTrace();
}
}
}
運行測試一下
遇到了我們指定的需要重試的異常,進行重試,間隔是 3 秒
-
retryIfException:
retryIfException
,拋出runtime
異常、checked
異常時都會重試,但是拋出error
不會重試。 -
retryIfRuntimeException:
retryIfRuntimeException
只會在拋runtime
異常的時候才重試,checked
異常和error
都不重試。 -
retryIfExceptionOfType:
retryIfExceptionOfType
允許我們只在發生特定異常的時候才重試,比如NullPointerException
和IllegalStateException
都屬於runtime
異常,也包括自定義的error
。
如:
retryIfExceptionOfType(NullPointerException.class)// 只在拋出空指針異常重試
- retryIfResult:
retryIfResult
可以指定你的Callable
方法在返回值的時候進行重試,如
// 返回false重試
.retryIfResult(Predicates.equalTo(false))
//以_error結尾才重試
.retryIfResult(Predicates.containsPattern("_error$"))
//返回爲空時重試
.retryIfResult(res-> res==null)
- **RetryListener:**當發生重試之後,假如我們需要做一些額外的處理動作,比如 log 一下異常,那麼可以使用
RetryListener
。每次重試之後,guava-retrying
會自動回調我們註冊的監聽。可以註冊多個RetryListener
,會按照註冊順序依次調用。
.withRetryListener(new RetryListener {
@Override
public <T> void onRetry(Attempt<T> attempt) {
logger.error("第【{}】次調用失敗" , attempt.getAttemptNumber());
}
}
)
總結
spring-retry 和 guava-retry 工具都是線程安全的重試,能夠支持併發業務場景的重試邏輯正確性。兩者都很好的將正常方法和重試方法進行了解耦,可以設置超時時間、重試次數、間隔時間、監聽結果、都是不錯的框架。
但是明顯感覺得到,guava-retry 在使用上更便捷,更靈活,能根據方法返回值來判斷是否重試,而 Spring-retry 只能根據拋出的異常來進行重試。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/rFaiO9yxsWbHuKD2pOs94w