前言
在日常工作中,隨著業(yè)務(wù)日漸龐大,不可避免的涉及到調(diào)用遠(yuǎn)程服務(wù),但是遠(yuǎn)程服務(wù)的健壯性和網(wǎng)絡(luò)穩(wěn)定性都是不可控因素,因此,我們需要考慮合適的重試機(jī)制去處理這些問題,最基礎(chǔ)的方式就是手動重試,侵入業(yè)務(wù)代碼去處理,再高端一點(diǎn)的通過切面去處理,較為優(yōu)雅的實(shí)現(xiàn)重試,下面,介紹兩個(gè)重試框架,只需要配置好重啟策略及重試任務(wù),即可使用。
重試任務(wù)
這里只是模擬傳參、相應(yīng)及異常,具體任務(wù)需對應(yīng)業(yè)務(wù)
package com.example.test.MessageRetry;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomUtils;
import org.springframework.remoting.RemoteAccessException;
@Slf4j
public class RetryTask {
/**
* 重試方法
* @param param
* @return
*/
public static boolean retryTask(String param){
log.info("請求參數(shù):{}",param);
int i = RandomUtils.nextInt(0,15);
log.info("隨機(jī)數(shù):{}",i);
if(i == 0){
log.error("參數(shù)異常");
throw new IllegalArgumentException("參數(shù)異常");
}else if(i > 10){
log.error("訪問異常");
throw new RemoteAccessException("訪問異常");
}else if(i % 2 == 0){
log.info("成功");
return true;
}else{
log.info("失敗");
return false;
}
}
}
Spring-Retry
簡介
Spirng-Retry是Spring提供的一個(gè)重試框架,為spring程序提供聲明式重試支持,主要針對可能拋出異常的調(diào)用操作,進(jìn)行有策略的重試。可以通過代碼方式和注解方式實(shí)現(xiàn),主要由重試執(zhí)行者和兩個(gè)策略構(gòu)成:
- RetryTemplet:重試執(zhí)行者,可以設(shè)置重試策略(設(shè)置重試上線、如何重試)和回退策略(立即重試還是等待一段時(shí)間后重試,默認(rèn)立即重試,如果需要配置等待一段時(shí)間后重試則需要指定回退策略),通過Execute提交執(zhí)行操作,只有在調(diào)用時(shí)拋出指定配置的異常,才會執(zhí)行重試
- 重試策略:
策略 | 方式 |
---|---|
NeverRetryPolicy | 只允許調(diào)用RetryCallback一次,不允許重試 |
AlwaysRetryPolicy | 允許無限重試,直到成功,此方式邏輯不當(dāng)會導(dǎo)致死循環(huán) |
SimpleRetryPolicy | 固定次數(shù)重試策略,默認(rèn)重試最大次數(shù)為3次,RetryTemplate默認(rèn)使用的策略 |
TimeoutRetryPolicy | 超時(shí)時(shí)間重試策略,默認(rèn)超時(shí)時(shí)間為1秒,在指定的超時(shí)時(shí)間內(nèi)允許重試 |
ExceptionClassifierRetryPolicy | 設(shè)置不同異常的重試策略,類似組合重試策略,區(qū)別在于這里只區(qū)分不同異常的重試 |
CircuitBreakerRetryPolicy | 有熔斷功能的重試策略,需設(shè)置3個(gè)參數(shù)openTimeout、resetTimeout和delegate |
CompositeRetryPolicy | 組合重試策略,有兩種組合方式,樂觀組合重試策略是指只要有一個(gè)策略允許即可以重試,悲觀組合重試策略是指只要有一個(gè)策略不允許即可以重試,但不管哪種組合方式,組合中的每一個(gè)策略都會執(zhí)行 |
- 重試回退策略:
回退策略 | 方式 |
---|---|
NoBackOffPolicy | 無退避算法策略,每次重試時(shí)立即重試 |
FixedBackOffPolicy | 固定時(shí)間的退避策略,需設(shè)置參數(shù)sleeper和backOffPeriod,sleeper指定等待策略,默認(rèn)是Thread.sleep,即線程休眠,backOffPeriod指定休眠時(shí)間,默認(rèn)1秒 |
UniformRandomBackOffPolicy | 隨機(jī)時(shí)間退避策略,需設(shè)置sleeper、minBackOffPeriod和maxBackOffPeriod,該策略在minBackOffPeriod,maxBackOffPeriod之間取一個(gè)隨機(jī)休眠時(shí)間,minBackOffPeriod默認(rèn)500毫秒,maxBackOffPeriod默認(rèn)1500毫秒 |
ExponentialBackOffPolicy | 指數(shù)退避策略,需設(shè)置參數(shù)sleeper、initialInterval、maxInterval和multiplier,initialInterval指定初始休眠時(shí)間,默認(rèn)100毫秒,maxInterval指定最大休眠時(shí)間,默認(rèn)30秒,multiplier指定乘數(shù),即下一次休眠時(shí)間為當(dāng)前休眠時(shí)間*multiplier |
ExponentialRandomBackOffPolicy | 隨機(jī)指數(shù)退避策略,引入隨機(jī)乘數(shù)可以實(shí)現(xiàn)隨機(jī)乘數(shù)回退 |
- 此外,還需要配置重試時(shí)間間隔、最大重試次數(shù)以及可重試異常
實(shí)現(xiàn)
代碼方式
RetryTemplate通過execute提交執(zhí)行操作,需要準(zhǔn)備RetryCallback和RecoveryCallback兩個(gè)類實(shí)例,前者對應(yīng)的就是重試回調(diào)邏輯實(shí)例,包裝正常的功能操作,RecoveryCallback實(shí)現(xiàn)的是整個(gè)執(zhí)行操作結(jié)束的恢復(fù)操作實(shí)例,只有在調(diào)用的時(shí)候拋出了異常,并且異常是在exceptionMap中配置的異常,才會執(zhí)行重試操作,否則就調(diào)用到excute方法的第二個(gè)執(zhí)行方法RecoveryCallback中
package com.example.test.MessageRetry;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.remoting.RemoteAccessException;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.retry.backoff.FixedBackOffPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service
@Slf4j
public class SpringRetryService {
/**
* 重試時(shí)間間隔ms,默認(rèn)1000ms
*/
private long retryPeriodTime = 5000L;
/**
* 最大重試次數(shù)
*/
private int maxRetryNum = 3;
/**
* 哪些結(jié)果和異常要重試,key表示異常類型,value表示是否需要重試
*/
private Map<Class<? extends Throwable>,Boolean> retryMap = new HashMap<>();
@Test
public void test(){
retryMap.put(IllegalArgumentException.class,true);
retryMap.put(RemoteAccessException.class,true);
//構(gòu)建重試模板
RetryTemplate retryTemplate = new RetryTemplate();
//設(shè)置重試回退操作策略,主要設(shè)置重試時(shí)間間隔
FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
backOffPolicy.setBackOffPeriod(retryPeriodTime);
//設(shè)置重試策略,主要設(shè)置重試次數(shù)
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(maxRetryNum,retryMap);
retryTemplate.setRetryPolicy(retryPolicy);
retryTemplate.setBackOffPolicy(backOffPolicy);
Boolean execute = retryTemplate.execute(
//RetryCallback
retryContext -> {
boolean b = RetryTask.retryTask("aaa");
log.info("調(diào)用結(jié)果:{}",b);
return b;
},
retryContext -> {
//RecoveryCallback
log.info("到達(dá)最多嘗試次數(shù)");
return false;
}
);
log.info("執(zhí)行結(jié)果:{}",execute);
}
}
注解方式
上面我們說到Spring-Retry是Spring提供的,那么,它就支持依賴整合
<!--spring-retry-->
<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>
然后,在啟動類上添加開啟注解
//表示是否開啟重試,屬性proxyTargetClass,boolean類型,是否創(chuàng)建基于子類(CGLIB)的代理,而不是標(biāo)準(zhǔn)的基于接口的代理,默認(rèn)false
@EnableRetry
package com.example.test.MessageRetry;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomUtils;
import org.springframework.remoting.RemoteAccessException;
@Slf4j
public class RetryTask {
/**
* 重試方法
* @param param
* @return
*/
public static boolean retryTask(String param){
log.info("請求參數(shù):{}",param);
int i = RandomUtils.nextInt(0,15);
log.info("隨機(jī)數(shù):{}",i);
if(i >-1){
log.error("參數(shù)異常");
throw new IllegalArgumentException("參數(shù)異常");
// }else if(i > 10){
// log.error("訪問異常");
// throw new RemoteAccessException("訪問異常");
// }else if(i % 2 == 0){
// log.info("成功");
// return true;
}else{
log.info("失敗");
return false;
}
}
}
/**
* 重試調(diào)用方法
* @param param
* @return
* @Retryable注解:
*
*/
@Retryable(value = {RemoteAccessException.class,IllegalArgumentException.class},maxAttempts = 3,backoff = @Backoff(delay = 5000L,multiplier = 2))
public void call(String param){
RetryTask.retryTask(param);
}
/**
* 達(dá)到最大重試次數(shù),或拋出了沒有指定的異常
* @param e
* @param param
* @return
*/
@Recover
public void recover(Exception e,String param){
log.error("達(dá)到最大重試次數(shù)!?。。?!");
}
@Test
public void retry(){
springRetryService.call("aaa");
}
- @Retryable注解說明
屬性 | 說明 |
---|---|
value | 指定重試的異常類型,默認(rèn)為空 |
maxAttempts | 最大嘗試次數(shù),默認(rèn)3次 |
include | 和value一樣,默認(rèn)為空,當(dāng)exclude也為空時(shí),默認(rèn)所有異常 |
exclude | 指定不處理的異常 |
Backoff | 重試策略 |
- @Backoff注解說明:設(shè)定重試倍數(shù),每次重試時(shí)間是上次的n倍
屬性 | 說明 |
---|---|
delay | 重試之間的等待時(shí)間(以毫秒為單位),默認(rèn)0 |
maxDelay | 重試之間的最大等待時(shí)間(以毫秒為單位),默認(rèn)0 |
multiplier | 延遲的倍數(shù),默認(rèn)0.0 |
delayExpression | 重試之間的等待時(shí)間表達(dá)式,默認(rèn)空 |
maxDelayExpression | 重試之間的最大等待時(shí)間表達(dá)式,默認(rèn)空 |
multiplierExpression | 指定延遲的倍數(shù)表達(dá)式,默認(rèn)空 |
random | 隨機(jī)指定延遲時(shí)間,默認(rèn)false |
-
@Recover注解說明:當(dāng)重試到達(dá)指定次數(shù)時(shí),將要回調(diào)的方法
-
@Retryable和@Recover修飾的方法要在同一個(gè)類中,且被@Retryable和@Recover標(biāo)記的方法不能有返回值,這樣Recover方法才會生效。由于@Retryable注解是通過切面實(shí)現(xiàn)的,因此我們要避免@Retryable 注解的方法的調(diào)用方和被調(diào)用方處于同一個(gè)類中,因?yàn)檫@樣會使@Retryable 注解失效。
我們可以看到,Spring-Retry只能針對指定異常重試,不能根據(jù)執(zhí)行結(jié)果返回值重試,整體使用也比較死板,下面,看下更加靈活的Guava-Retry。
Guava-Retry
簡介
Guava-Retry是谷歌的Guava庫的一個(gè)小擴(kuò)展,允許為任意函數(shù)調(diào)用創(chuàng)建可配置的重試策略,我們可以通過構(gòu)建重試實(shí)例RetryBuilder,來設(shè)置重試源、配置重試次數(shù)、重試超時(shí)時(shí)間、等待時(shí)間間隔等,實(shí)現(xiàn)優(yōu)雅的重試機(jī)制。
- 主要屬性
屬性 | 說明 |
---|---|
attemptTimeLimiter | 時(shí)間限制策略,單次任務(wù)執(zhí)行時(shí)間限制,超時(shí)終止 |
stopStrategy | 停止重試策略 |
waitStrategy | 等待策略 |
blockStrategy | 任務(wù)阻塞策略,即當(dāng)前任務(wù)執(zhí)行完,下次任務(wù)執(zhí)行前做什么,僅有線程阻塞threadSleepStrategy |
retryException | 重試異常(重試策略) |
listeners | 自定義重試監(jiān)聽器,可用于記錄日志等 |
- 時(shí)間限制策略
策略 | 說明 |
---|---|
NoAttemptTimeLimit | 對代理方法不添加時(shí)間限制,默認(rèn) |
FixedAttemptTimeLimit | 對代理方法的嘗試添加固定時(shí)間限制 |
- 重試策略(重試異常)
策略 | 說明 |
---|---|
retryIfException | 拋出 runtime 異常、checked 異常時(shí)都會重試,但是拋出 error 不會重試 |
retryIfRuntimeException | 只會在拋 runtime 異常的時(shí)候才重試,checked 異常和error 都不重試 |
retryIfExceptionOfType | 允許我們只在發(fā)生特定異常的時(shí)候才重試,比如NullPointerException 和 IllegalStateException 都屬于 runtime 異常,也包括自定義的error |
retryIfResult | 可以指定你的 Callable 方法在返回值的時(shí)候進(jìn)行重試 |
- 停止策略
策略 | 說明 |
---|---|
StopAfterDelayStrategy | 設(shè)定最長執(zhí)行時(shí)間,無論任務(wù)執(zhí)行幾次,一旦超時(shí),任務(wù)終止,返回RetryException |
StopAfterAttemptStrategy | 設(shè)定最大嘗試次數(shù),一旦超過,返回重試異常 |
NeverStopStrategy | 一直輪詢直到獲取期望結(jié)果 |
- 等待策略
策略 | 說明 |
---|---|
ExceptionWaitStrategy | 異常時(shí)長等待,如果拋出的是指定異常,則從傳入的方法中取得等待時(shí)間并返回;如果異常不匹配,則返回等待時(shí)間為0L |
CompositeWaitStrategy | 復(fù)合時(shí)長等待,在獲取等待時(shí)間時(shí)會獲取多種等待策略各自的等待時(shí)間,然后累加這些等待時(shí)間 |
FibonacciWaitStrategy | 斐波那契等待策略 |
ExponentialWaitStrategy | 指數(shù)等待時(shí)長,指數(shù)增長,若設(shè)置了最大時(shí)間,則停止,否則到Long.MAX_VALUE |
IncrementingWaitStrategy | 遞增等待,提供一個(gè)初始時(shí)長和步長,隨次數(shù)疊加 |
RandomWaitStrategy | 隨機(jī)等待時(shí)長,可以提供一個(gè)最大和最小時(shí)間,從范圍內(nèi)隨機(jī) |
FixedWaitStrategy | 固定等待時(shí)長 |
代碼
<!--guava-retryer-->
<dependency>
<groupId>com.github.rholder</groupId>
<artifactId>guava-retrying</artifactId>
<version>2.0.0</version>
</dependency>
package com.example.test.MessageRetry;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomUtils;
import org.springframework.remoting.RemoteAccessException;
@Slf4j
public class RetryTask {
/**
* 重試方法
* @param param
* @return
*/
public static boolean retryTask(String param){
log.info("請求參數(shù):{}",param);
int i = RandomUtils.nextInt(0,15);
log.info("隨機(jī)數(shù):{}",i);
if(i < 3){
log.error("參數(shù)異常");
throw new IllegalArgumentException("參數(shù)異常");
}else if(i > 10){
log.error("訪問異常");
throw new RemoteAccessException("訪問異常");
}else if(i % 2 == 0){
log.info("成功");
return true;
}else{
log.info("失敗");
return false;
}
}
}
package com.example.test.MessageRetry;
import com.github.rholder.retry.*;
import com.google.common.base.Predicates;
import lombok.extern.slf4j.Slf4j;
import org.springframework.remoting.RemoteAccessException;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
@Slf4j
public class GuavaRetryService {
public void guavaRetry(){
//構(gòu)建重試實(shí)例RetryBuilder,可以設(shè)置重試源,可以配置重試次數(shù)、重試超時(shí)時(shí)間、等待時(shí)間間隔
Retryer<Boolean>retryer = RetryerBuilder.<Boolean>newBuilder()
//設(shè)置異常重試源
.retryIfExceptionOfType(RemoteAccessException.class)
.retryIfExceptionOfType(IllegalArgumentException.class)
//設(shè)置根據(jù)結(jié)果重試 res->res==false Predicates.containsPattern("_error$")
.retryIfResult(Predicates.equalTo(false))
//設(shè)置等待時(shí)間
.withWaitStrategy(WaitStrategies.fixedWait(3, TimeUnit.SECONDS))
//設(shè)置最大重試次數(shù)
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
//設(shè)置重試監(jiān)聽,可用作重試時(shí)的額外動作
.withRetryListener(new RetryListener() {
@Override
public <V> void onRetry(Attempt<V> attempt) {
log.error("第【{}】次調(diào)用失敗",attempt.getAttemptNumber());
}
})
//設(shè)置阻塞策略
.withBlockStrategy(BlockStrategies.threadSleepStrategy())
//設(shè)置時(shí)間限制
.withAttemptTimeLimiter(AttemptTimeLimiters.noTimeLimit())
.build();
try{
retryer.call(()->RetryTask.retryTask("aaa"));
}catch (Exception e){
e.printStackTrace();
}
}
}
可以看到,我們設(shè)置了重試三次,超過這個(gè)限制沒有執(zhí)行成功,拋出了重試異常,而且也可以根據(jù)我們的返回結(jié)果來判斷。文章來源:http://www.zghlxwxcb.cn/news/detail-640230.html
總結(jié)
Spring-Retry和Guava-Retry都是線程安全的重試框架,能夠保證并發(fā)業(yè)務(wù)下重試邏輯的正確性。兩者都很好的將正常方法和重試方法進(jìn)行了解耦,可以設(shè)置超時(shí)時(shí)間、重試次數(shù)、間隔時(shí)間、監(jiān)聽結(jié)果等,相比來說,Guava-Retry比Spring-Retry更加靈活,并且可以通過返回值來進(jìn)行重試,兩者都是非常好的重試框架,具體的選用看相關(guān)的業(yè)務(wù)場景即可。文章來源地址http://www.zghlxwxcb.cn/news/detail-640230.html
到了這里,關(guān)于重試框架入門:Spring-Retry&Guava-Retry的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!