一棉磨、前言
由于最近項(xiàng)目中用到了guava-retrying
江掩,并且以前沒有遇到過這個(gè)工具包,所以準(zhǔn)備通過本篇文章來系統(tǒng)的梳理下該工具包的使用。
二环形、背景
??一般在各種業(yè)務(wù)場景中策泣,為了保持系統(tǒng)穩(wěn)定,我們都會(huì)有相應(yīng)的重試機(jī)制斟赚,因?yàn)楸热缯f着降,某個(gè)接口某個(gè)數(shù)據(jù)庫鏈接由于網(wǎng)絡(luò)抖動(dòng)或者其他因素導(dǎo)致響應(yīng)失敗差油,這時(shí)候直接判定失敗或者M(jìn)ock數(shù)據(jù)未必是一種優(yōu)雅的方式拗军,因?yàn)檫@種情況下未必是接口掛掉了或者數(shù)據(jù)庫連不上了,有可能是網(wǎng)絡(luò)一時(shí)的抖動(dòng)導(dǎo)致的蓄喇,所以這時(shí)候一個(gè)優(yōu)雅的重試機(jī)制或許能幫上我們发侵。
三、實(shí)現(xiàn)
guava-retrying
是Google Guava庫的一個(gè)擴(kuò)展包妆偏,可以為任意函數(shù)調(diào)用創(chuàng)建可配置的重試機(jī)制刃鳄。該擴(kuò)展包比較簡單,大約包含了10個(gè)方法和類:
不過可以看到github上該項(xiàng)目已經(jīng)好多年沒有維護(hù)了钱骂,但這并不影響它的使用叔锐,因?yàn)樗呀?jīng)足夠穩(wěn)定了。接下來见秽,我們直接來根據(jù)個(gè)例子來學(xué)習(xí)愉烙。
首先,看下Maven配置:
<dependency>
<groupId>com.github.rholder</groupId>
<artifactId>guava-retrying</artifactId>
<version>2.0.0</version>
</dependency>
因?yàn)間uava-retrying是基于Google的核心類庫guava的重試機(jī)制實(shí)現(xiàn)解取,所以需要依賴guava的包步责,這里記得引入下。例子如下:
Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
.retryIfException()
.retryIfResult(aBoolean -> Objects.equals(aBoolean, false))
.withAttemptTimeLimiter(AttemptTimeLimiters.fixedTimeLimit(10, TimeUnit.SECONDS, Executors.newCachedThreadPool()))
.withWaitStrategy(WaitStrategies.fixedWait(5, TimeUnit.SECONDS))
.withStopStrategy(StopStrategies.stopAfterAttempt(5))
.withRetryListener(new RetryListener() {
@Override
public <V> void onRetry(Attempt<V> attempt) {
System.out.print("retry time=" + attempt.getAttemptNumber());
}
}).build();
try {
retryer.call(() -> {
// 邏輯處理
return null;
});
} catch (Exception e) {
System.out.println("exception:" + e);
}
這就是一個(gè)重試機(jī)制的實(shí)現(xiàn)了禀苦,比較簡單蔓肯,我們來看下具體接口及相應(yīng)的策咯。
- newBuilder:創(chuàng)建RetryerBuilder對(duì)象振乏,通過該類進(jìn)行構(gòu)建各種重試策咯蔗包;
- retryIfException:拋出異常時(shí)重試,但拋出error不會(huì)重試慧邮;另外該方法還包含一個(gè)重載的方法调限,可以自定義針對(duì)異常的實(shí)現(xiàn);
- retryIfRuntimeException:見名知義赋咽,拋出RuntimeException時(shí)重試旧噪;
- retryIfExceptionOfType:拋出指定異常類型時(shí)重試;
- retryIfResult:根據(jù)具體的返回值選擇重試脓匿;
- withRetryListener:在重試的時(shí)候進(jìn)行事件監(jiān)聽淘钟,這中間我們可以記錄下錯(cuò)誤日志什么的;可以注冊(cè)多個(gè)事件監(jiān)聽器陪毡,會(huì)按照注冊(cè)順序依次調(diào)用米母;
- withWaitStrategy:重試等待策略勾扭,核心策咯之一;
- withStopStrategy:重試停止策略铁瞒,核心策咯之一妙色;
- withBlockStrategy:重試阻塞策略,也就是兩次重試的時(shí)間間隔的實(shí)現(xiàn)方式慧耍;
- withAttemptTimeLimiter:單次任務(wù)執(zhí)行時(shí)長限制(如果單次任務(wù)執(zhí)行超時(shí)身辨,則終止執(zhí)行當(dāng)前任務(wù))(該方法因SimpleTimeLimiter構(gòu)造函數(shù)變更已失效無法使用);
- build:通過newBuilder構(gòu)建了各種重試策咯芍碧,構(gòu)建完成煌珊,還需要通過build方法借助Retryer來執(zhí)行;
接下來泌豆,我們來看一下主要的幾個(gè)策咯及核心類定庵。
1. Attemp
Attemp既是一次任務(wù)重試(call),也是一次請(qǐng)求的結(jié)果踪危,記錄了當(dāng)前請(qǐng)求的重試次數(shù)蔬浙,是否包含異常和請(qǐng)求的返回值。我們可以配合監(jiān)聽器使用贞远,用于記錄重試過程的細(xì)節(jié)畴博,常用的方法有如下幾個(gè):
- getAttemptNumber(),表示準(zhǔn)備開始第幾次重試兴革;
- getDelaySinceFirstAttempt()绎晃,表示距離第一次重試的延遲,也就是與第一次重試的時(shí)間差杂曲,單位毫秒庶艾;
- hasException(),表示是異常導(dǎo)致的重試還是正常返回擎勘;
- hasResult()咱揍,表示是否返回了結(jié)果;因?yàn)橛袝r(shí)候是因?yàn)榉祷亓颂囟ńY(jié)果才進(jìn)行重試棚饵;
- getExceptionCause()煤裙,如果是異常導(dǎo)致的重試,那么獲取具體具體的異常類型噪漾;
- getResult()硼砰,返回重試的結(jié)果;
- get()欣硼,如果有的話题翰,返回重試的結(jié)果;和
getResult
不同的在于對(duì)異常的處理;
2. Retryer
Retryer是最核心的類豹障,是用于執(zhí)行重試策咯的類冯事,通過RetryerBuilder類進(jìn)行構(gòu)造,并且RetryerBuilder負(fù)責(zé)將設(shè)置好的重試策咯添加到Retryer中血公,最終通過執(zhí)行Retryer的核心方法call
來執(zhí)行重試策咯:
public V call(Callable<V> callable) throws ExecutionException, RetryException {
long startTime = System.nanoTime();
// 重試次數(shù)
for (int attemptNumber = 1; ; attemptNumber++) {
Attempt<V> attempt;
try {
// attemptTimeLimiter會(huì)設(shè)置業(yè)務(wù)執(zhí)行的時(shí)長限制
V result = attemptTimeLimiter.call(callable);
// 根據(jù)上次執(zhí)行結(jié)果構(gòu)建新的重試對(duì)象
attempt = new ResultAttempt<V>(result, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
} catch (Throwable t) {
// 如果有異常昵仅,構(gòu)建新的異常重試對(duì)象
attempt = new ExceptionAttempt<V>(t, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
}
// 循環(huán)遍歷監(jiān)聽器
for (RetryListener listener : listeners) {
listener.onRetry(attempt);
}
// 判斷是否滿足重試條件,來決定是否繼續(xù)等待并進(jìn)行重試
if (!rejectionPredicate.apply(attempt)) {
return attempt.get();
}
// 達(dá)到停止重試策略累魔,但還沒有結(jié)果摔笤,拋出異常
if (stopStrategy.shouldStop(attempt)) {
throw new RetryException(attemptNumber, attempt);
} else {
// 獲取等待策略中設(shè)置的重試的時(shí)長
long sleepTime = waitStrategy.computeSleepTime(attempt);
try {
// 阻塞策略進(jìn)行阻塞
blockStrategy.block(sleepTime);
} catch (InterruptedException e) {
// 線程中斷,拋出異常
Thread.currentThread().interrupt();
throw new RetryException(attemptNumber, attempt);
}
}
}
}
3. WaitStrategies 重試等待策略
3.1 ExponentialWaitStrategy 指數(shù)等待策略
指數(shù)補(bǔ)償 算法 Exponential Backoff
.withWaitStrategy(WaitStrategies.exponentialWait(100, 5, TimeUnit.MINUTES))
創(chuàng)建一個(gè)永久重試的重試器薛夜,每次重試失敗時(shí)以遞增的指數(shù)時(shí)間等待籍茧,直到最多5分鐘版述。 5分鐘后梯澜,每隔5分鐘重試一次。對(duì)該例而言:
第一次失敗后渴析,依次等待時(shí)長:2^1 * 100;2^2 * 100晚伙;2^3 * 100;...
在ExponentialWaitStrategy中,根據(jù)重試次數(shù)計(jì)算等待時(shí)長的源碼我們可以關(guān)注下:
@Override
public long computeSleepTime(Attempt failedAttempt) {
double exp = Math.pow(2, failedAttempt.getAttemptNumber());
long result = Math.round(multiplier * exp);
if (result > maximumWait) {
result = maximumWait;
}
return result >= 0L ? result : 0L;
}
如果以后有類似的需求俭茧,我們可以自己寫下這些算法咆疗,而有關(guān)更多指數(shù)補(bǔ)償 算法 Exponential Backoff,可以參考:http://en.wikipedia.org/wiki/Exponential_backoff
3.2 FibonacciWaitStrategy 斐波那契等待策略
Fibonacci Backoff 斐波那契補(bǔ)償算法
.withWaitStrategy(WaitStrategies.fibonacciWait(100, 2, TimeUnit.MINUTES))
創(chuàng)建一個(gè)永久重試的重試器母债,每次重試失敗時(shí)以斐波那契數(shù)列來計(jì)算等待時(shí)間午磁,直到最多2分鐘;2分鐘后毡们,每隔2分鐘重試一次迅皇;對(duì)該例而言:
第一次失敗后,依次等待時(shí)長:1*100;1*100衙熔;2*100登颓;3*100;5*100红氯;...
3.3 FixedWaitStrategy 固定時(shí)長等待策略
withWaitStrategy(WaitStrategies.fixedWait(10, TimeUnit.SECONDS))
固定時(shí)長等待策略框咙,失敗后,將等待固定的時(shí)長進(jìn)行重試痢甘;
3.4 RandomWaitStrategy 隨機(jī)時(shí)長等待策略
withWaitStrategy(WaitStrategies.randomWait(10, TimeUnit.SECONDS));
withWaitStrategy(WaitStrategies.randomWait(1, TimeUnit.SECONDS, 10, TimeUnit.SECONDS));
隨機(jī)時(shí)長等待策略喇嘱,可以設(shè)置一個(gè)隨機(jī)等待的最大時(shí)長,也可以設(shè)置一個(gè)隨機(jī)等待的時(shí)長區(qū)間塞栅。
3.5 IncrementingWaitStrategy 遞增等待策略
withWaitStrategy(WaitStrategies.incrementingWait(1, TimeUnit.SECONDS, 5, TimeUnit.SECONDS))
遞增等待策略者铜,根據(jù)初始值和遞增值,等待時(shí)長依次遞增。就本例而言:
第一次失敗后王暗,將依次等待1s悔据;6s(1+5);11(1+5+5)s俗壹;16(1+5+5+5)s科汗;...
3.6 ExceptionWaitStrategy 異常等待策略
withWaitStrategy(WaitStrategies.exceptionWait(ArithmeticException.class, e -> 1000L))
根據(jù)所發(fā)生的異常指定重試的等待時(shí)長;如果異常不匹配绷雏,則等待時(shí)長為0头滔;
3.7 CompositeWaitStrategy 復(fù)合等待策略
.withWaitStrategy(WaitStrategies.join(WaitStrategies.exceptionWait(ArithmeticException.class, e -> 1000L),WaitStrategies.fixedWait(5, TimeUnit.SECONDS)))
復(fù)合等待策略;如果所執(zhí)行的程序滿足一個(gè)或多個(gè)等待策略涎显,那么等待時(shí)間為所有等待策略時(shí)間的總和坤检。
4. StopStrategies 重試停止策略
4.1 NeverStopStrategy
withStopStrategy(StopStrategies.neverStop())
一直不停止,一直需要重試期吓。
4.2 StopAfterAttemptStrategy
withStopStrategy(StopStrategies.stopAfterAttempt(3))
在重試次數(shù)達(dá)到最大次數(shù)之后早歇,終止任務(wù)。
4.3 StopAfterDelayStrategy
withStopStrategy(StopStrategies.stopAfterDelay(3, TimeUnit.MINUTES))
在重試任務(wù)達(dá)到設(shè)置的最長時(shí)長之后讨勤,無論任務(wù)執(zhí)行次數(shù)箭跳,都終止任務(wù)。
5. BlockStrategies 阻塞策略
阻塞策略默認(rèn)提供的只有一種:ThreadSleepStrategy潭千,實(shí)現(xiàn)方式是通過Thread.sleep(sleepTime)
來實(shí)現(xiàn)谱姓;不過這也給了我們極大的發(fā)揮空間,我們可以自己實(shí)現(xiàn)阻塞策略刨晴。
6. AttemptTimeLimiters 任務(wù)執(zhí)行時(shí)長限制
這個(gè)表示單次任務(wù)執(zhí)行時(shí)間限制(如果單次任務(wù)執(zhí)行超時(shí)屉来,則終止執(zhí)行當(dāng)前任務(wù));
6.1 NoAttemptTimeLimit 無時(shí)長限制
.withAttemptTimeLimiter(AttemptTimeLimiters.noTimeLimit())
顧名思義狈癞,不限制執(zhí)行時(shí)長茄靠;每次都是等執(zhí)行任務(wù)執(zhí)行完成之后,才進(jìn)行后續(xù)的重試策咯亿驾。
6.2 FixedAttemptTimeLimit
.withAttemptTimeLimiter(AttemptTimeLimiters.fixedTimeLimit(10, TimeUnit.SECONDS));
.withAttemptTimeLimiter(AttemptTimeLimiters.fixedTimeLimit(10, TimeUnit.SECONDS, Executors.newCachedThreadPool()));
可以指定任務(wù)的執(zhí)行時(shí)長限制嘹黔,并且為了控制線程管理,最好指定相應(yīng)的線程池莫瞬。
四儡蔓、總結(jié)
- guava-retrying功能強(qiáng)大,基本能滿足我們常用的操作疼邀;如果不滿足當(dāng)前各種已有的策咯喂江,可以選擇分別繼承
WaitStrategy
,StopStrategy
旁振,BlockStrategy
來自定義自己的實(shí)現(xiàn)获询;- guava-retrying默認(rèn)的阻塞策咯是通過
Thread.sleep
來實(shí)現(xiàn)的涨岁,也就是說通過讓當(dāng)前線程休眠來實(shí)現(xiàn)阻塞功能,這或許不是一種很好的選擇吉嚣;- 在實(shí)際使用過程種梢薪,我們可能會(huì)經(jīng)常要調(diào)整重試次數(shù)、重試時(shí)間等策咯尝哆,所以我們可以將重試策咯的配置進(jìn)行參數(shù)化保存秉撇,達(dá)到動(dòng)態(tài)調(diào)節(jié)的目的;另外在使用的時(shí)候秋泄,也可以封裝成util工具類供大家使用琐馆;
本文參考:
重試?yán)髦瓽uava Retrying
https://github.com/rholder/guava-retrying