SpringBoot系列(21):基于Guava_Retrying機(jī)制實(shí)現(xiàn)重試功能
作者:
修羅debug
版權(quán)聲明:本文為博主原創(chuàng)文章,遵循 CC 4.0 by-sa 版權(quán)協(xié)議,轉(zhuǎn)載請(qǐng)附上原文出處鏈接和本聲明。
摘要:
對(duì)于“接口/方法 重試”,相信很多小伙伴都聽(tīng)說(shuō)過(guò),但是在實(shí)際項(xiàng)目中卻很少真正去實(shí)踐過(guò),在本篇文章中,Debug將給各位小伙伴介紹一種“重試”機(jī)制的實(shí)現(xiàn),即Guava_Retrying,相對(duì)于傳統(tǒng)的Spring_Retrying或者動(dòng)態(tài)代理實(shí)現(xiàn)的重試功能而言,本文要介紹的Guava_Retrying機(jī)制使用起來(lái)將更加容易、靈活性更強(qiáng)!
內(nèi)容:
老趙:“這個(gè) 接口/方法 調(diào)用又失敗了,老李啊,你去寫(xiě)個(gè)重試功能吧!”。
老李:“他娘的,這接口調(diào)用咋又不行了。。。行吧,老子立馬給你擼一個(gè)重試功能” 。
這樣的對(duì)話,相信有些小伙伴會(huì)感覺(jué)似曾相識(shí)!特別是當(dāng)自己在工位上安安靜靜的寫(xiě)代碼時(shí),會(huì)突然性的接到技術(shù)老大分配給自己的這種需求。。。沒(méi)啥好說(shuō)的,只能潛下心,去研究研究了!
對(duì)于“重試”,那可是有場(chǎng)景限制的,不是什么場(chǎng)景都適合重試,比如參數(shù)校驗(yàn)不合法、寫(xiě)操作等(因?yàn)橐紤]到寫(xiě)是否冪等)都不適合重試。
而諸如“遠(yuǎn)程調(diào)用超時(shí)”、“網(wǎng)絡(luò)突然中斷”等業(yè)務(wù)場(chǎng)景則可以進(jìn)行重試,在微服務(wù) 治理框架中,通常都有自己的重試與超時(shí)配置,比如Dubbo可以設(shè)置retries=1,timeout=500調(diào)用失敗只重試1次,超過(guò)500ms調(diào)用仍未返回則調(diào)用失?。ㄔ斍榭梢杂^看學(xué)習(xí)Debug錄制的“分布式服務(wù)調(diào)度Dubbo實(shí)戰(zhàn)教程 https://www.fightjava.com/web/index/course/detail/2 ”)
對(duì)于“外部 RPC 調(diào)用”,或者“數(shù)據(jù)入庫(kù)”等操作,如果一次操作失敗,則可以進(jìn)行多次重試,從而提高調(diào)用成功的可能性。
下面我們基于前面搭建的SpringBoot多模塊企業(yè)級(jí)項(xiàng)目,基于Guava_Retrying初步實(shí)現(xiàn)所謂的“重試功能”。工欲善其事必先利其器,首先當(dāng)然是需要加入Guava_Retrying的依賴Jar,如下所示:
<!--guava-retrying-->
<dependency>
<groupId>com.github.rholder</groupId>
<artifactId>guava-retrying</artifactId>
<version>2.0.0</version>
</dependency>
之后,我們來(lái)寫(xiě)個(gè)簡(jiǎn)單的入門(mén)案例,先來(lái) 過(guò)一把“接口調(diào)用重試”的癮!
/**
* Guava_retrying機(jī)制實(shí)現(xiàn)重試
* @Author:debug (SteadyJack)
* @Link: weixin-> debug0868 qq-> 1948831260
* @Date: 2019/12/1 16:17
**/
public class RetryUtil {
private static final Logger log= LoggerFactory.getLogger(RetryUtil.class);
private static Integer i=1;
public static Integer execute() throws Exception{
log.info("----重試時(shí) 變量i的疊加邏輯----");
return i++;
}
public static void main(String[] args) {
//TODO:定義任務(wù)實(shí)例
Callable<String> callable= () -> {
Integer res=execute();
//當(dāng)重試達(dá)到3 + 1次之后 我們就不玩了
if (res>3){
return res.toString();
}
return null;
};
//TODO:定義重試器
Retryer<String> retryer=RetryerBuilder.<String>newBuilder()
//TODO:當(dāng)返回結(jié)果為Null時(shí) - 執(zhí)行重試
.retryIfResult(Predicates.isNull())
//TODO:當(dāng)執(zhí)行核心業(yè)務(wù)邏輯拋出RuntimeException - 執(zhí)行重試
.retryIfRuntimeException()
//TODO:還可以自定義拋出何種異常時(shí) - 執(zhí)行重試
.retryIfExceptionOfType(IOException.class)
.build();
try {
retryer.call(callable);
} catch (ExecutionException | RetryException e) {
e.printStackTrace();
}
}
}
運(yùn)行該main方法,可以得到如下的結(jié)果:
從該上述代碼中,我們得知“重試機(jī)制”功能實(shí)現(xiàn)的核心在于定義Retryer實(shí)例以及Callable任務(wù)運(yùn)行實(shí)例 ,特別是Retryer實(shí)例,可以設(shè)置“什么時(shí)機(jī)重試”。
除此之外,對(duì)于 Retryer實(shí)例 我們還可以設(shè)置“重試的次數(shù)”、“重試的時(shí)間間隔”、“每次重試時(shí),定義Listener監(jiān)聽(tīng)一些操作邏輯”等等。如下代碼所示:
public static void main(String[] args) {
//TODO:定義任務(wù)實(shí)例
Callable<String> callable= () -> {
return null;
};
//TODO:每次重試時(shí) 監(jiān)聽(tīng)器執(zhí)行的邏輯
RetryListener retryListener=new RetryListener() {
@Override
public <V> void onRetry(Attempt<V> attempt) {
Long curr=attempt.getAttemptNumber();
log.info("----每次重試時(shí) 監(jiān)聽(tīng)器執(zhí)行的邏輯,當(dāng)前已經(jīng)是第 {} 次重試了----",curr);
if (curr == 3){
log.error("--重試次數(shù)已到,是不是得該執(zhí)行一些補(bǔ)償邏輯,如發(fā)送短信、發(fā)送郵件...");
}
}
};
//TODO:定義重試器
Retryer<String> retryer=RetryerBuilder.<String>newBuilder()
//TODO:當(dāng)返回結(jié)果為Null時(shí) - 執(zhí)行重試
.retryIfResult(Predicates.isNull())
//TODO:當(dāng)執(zhí)行核心業(yè)務(wù)邏輯拋出RuntimeException - 執(zhí)行重試
.retryIfRuntimeException()
//TODO:還可以自定義拋出何種異常時(shí) - 執(zhí)行重試
.retryIfExceptionOfType(IOException.class)
//TODO:每次重試時(shí)的時(shí)間間隔為5s
.withWaitStrategy(WaitStrategies.fixedWait(5L, TimeUnit.SECONDS))
//TODO:重試次數(shù)為3次,3次之后就不重試了
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
//TODO:每次重試時(shí)定義一個(gè)監(jiān)聽(tīng)器listener,監(jiān)聽(tīng)器的邏輯可以是 "日志記錄"、"做一些補(bǔ)償操作"...
.withRetryListener(retryListener)
.build();
try {
retryer.call(callable);
} catch (ExecutionException | RetryException e) {
e.printStackTrace();
}
}
其中,我們加入了“監(jiān)聽(tīng)器Listener”、“定義了重試次數(shù)”、“定義了每次重試的時(shí)間間隔”,這三個(gè)才是Guava_Retrying提供給開(kāi)發(fā)者重量級(jí)的玩意,如下代碼所示!
//TODO:每次重試時(shí)的時(shí)間間隔為5s
.withWaitStrategy(WaitStrategies.fixedWait(5L, TimeUnit.SECONDS))
//TODO:重試次數(shù)為3次,3次之后就不重試了
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
//TODO:每次重試時(shí)定義一個(gè)監(jiān)聽(tīng)器listener,監(jiān)聽(tīng)器的邏輯可以是 "日志記錄"、"做一些補(bǔ)償操作"...
.withRetryListener(retryListener)
其中,“重試次數(shù)策略StopStrategies”、“重試的時(shí)間間隔設(shè)置策略WaitStrategies”中Guava_Retrying提供了許多種選擇,比如“重試次數(shù)可以是一個(gè)隨機(jī)數(shù)”、“重試的時(shí)間間隔也可以設(shè)置為某個(gè)區(qū)間范圍內(nèi)的隨機(jī)數(shù)”等等,下圖為運(yùn)行結(jié)果截圖:
下面,我們來(lái)擼一個(gè)真實(shí)的業(yè)務(wù)場(chǎng)景,即“調(diào)用某個(gè)接口的方法,用于獲取SysConfig配置表中某個(gè)字典配置記錄,如果該字典配置記錄不存在(即返回Null),那我們就重試3次,如果期間獲取到了,那么就返回結(jié)果;3次過(guò)后,依舊為Null時(shí),則執(zhí)行一些補(bǔ)償性的措施:即發(fā)送郵件通知給到指定的人員,讓他們上去檢查檢查相應(yīng)的數(shù)據(jù)狀況!”
下圖為 系統(tǒng)字典配置表SysConfig存儲(chǔ)的字典記錄,其中,沒(méi)有id=11的記錄,我們將拿著這個(gè) id=11 來(lái)進(jìn)行測(cè)試:
如下代碼為正常項(xiàng)目開(kāi)發(fā)過(guò)程中我們自定義的Service及其方法:
/**
* Guava_Retrying重試機(jī)制的 小型真實(shí)案例
* @Author:debug (SteadyJack)
* @Link: weixin-> debug0868 qq-> 1948831260
* @Date: 2019/12/1 17:51
**/
@Service
public class RetryService {
private static final Logger log= LoggerFactory.getLogger(RetryService.class);
@Autowired
private SysConfigMapper sysConfigMapper;
@Autowired
private EmailSendService emailSendService;
//TODO:獲取某個(gè)字典配置詳情
public SysConfig getConfigInfo(final Integer id){
SysConfig config=sysConfigMapper.selectByPrimaryKey(id);
if (config==null){
//TODO:當(dāng)沒(méi)有查詢到該數(shù)據(jù)記錄時(shí),執(zhí)行重試邏輯
doRetry(id);
config=sysConfigMapper.selectByPrimaryKey(id);
}
return config;
}
//TODO:執(zhí)行重試邏輯
private void doRetry(final Integer id){
//TODO:定義任務(wù)實(shí)例
Callable<SysConfig> callable= () -> {
return sysConfigMapper.selectByPrimaryKey(id);
};
//TODO:每次重試時(shí) 監(jiān)聽(tīng)器執(zhí)行的邏輯
RetryListener retryListener=new RetryListener() {
@Override
public <V> void onRetry(Attempt<V> attempt) {
Long curr=attempt.getAttemptNumber();
log.info("----每次重試時(shí) 監(jiān)聽(tīng)器執(zhí)行的邏輯,當(dāng)前已經(jīng)是第 {} 次重試了----",curr);
//當(dāng)達(dá)到3次時(shí) 就執(zhí)行一些補(bǔ)償性的措施,如發(fā)送郵件通知某些大佬….
if (curr == 3){
log.error("--重試次數(shù)已到,是不是得該執(zhí)行一些補(bǔ)償邏輯,如發(fā)送短信、發(fā)送郵件...");
emailSendService.sendSimpleEmail("重試次數(shù)已到","請(qǐng)各位大佬上去檢查一下sysConfig是否存在","1948831260@qq.com");
}
}
};
//TODO:定義重試器
Retryer<SysConfig> retryer= RetryerBuilder.<SysConfig>newBuilder()
//TODO:當(dāng)返回結(jié)果為 false 時(shí) - 執(zhí)行重試(即sysCofig為null)
.retryIfResult(Objects::isNull)
//TODO:當(dāng)執(zhí)行核心業(yè)務(wù)邏輯拋出RuntimeException - 執(zhí)行重試
.retryIfRuntimeException()
//TODO:還可以自定義拋出何種異常時(shí) - 執(zhí)行重試
.retryIfExceptionOfType(IOException.class)
//TODO:每次重試時(shí)的時(shí)間間隔為10s (當(dāng)然啦,實(shí)際項(xiàng)目中一般是不超過(guò)1s的,如500ms,這里是為了方便模擬演示)
.withWaitStrategy(WaitStrategies.fixedWait(10L, TimeUnit.SECONDS))
//TODO:重試次數(shù)為3次,3次之后就不重試了
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
//TODO:每次重試時(shí)定義一個(gè)監(jiān)聽(tīng)器listener,監(jiān)聽(tīng)器的邏輯可以是 "日志記錄"、"做一些補(bǔ)償操作"...
.withRetryListener(retryListener)
.build();
try {
retryer.call(callable);
} catch (ExecutionException | RetryException e) {
e.printStackTrace();
}
}
}
最后寫(xiě)個(gè)Java Unit Test,即Java單元測(cè)試案例,如下所示:
@Autowired
private RetryService retryService;
@Test
public void method8() throws Exception{
final Integer id=11;
SysConfig entity=retryService.getConfigInfo(id);
log.info("---結(jié)果:{}",entity);
}
點(diǎn)擊運(yùn)行該單元測(cè)試案例,啥事都不要做,等待運(yùn)行結(jié)果,你會(huì)發(fā)現(xiàn)“重試”的效果我們已經(jīng)實(shí)現(xiàn)了!如下所示:
我們?cè)冱c(diǎn)擊運(yùn)行該單元測(cè)試案例,然后在它運(yùn)行了第1次重試機(jī)會(huì)之后,我們趕緊手動(dòng)到數(shù)據(jù)庫(kù)將 id=12 的那條系統(tǒng)配置記錄,調(diào)整為 id=11 !然后再來(lái)看運(yùn)行的結(jié)果,如下圖所示:
如下圖為“補(bǔ)償性措施”中的“發(fā)送郵件”:
好了,本篇文章我們就介紹到這里了,建議各位小伙伴一定要照著文章提供的樣例代碼擼一擼,只有擼過(guò)才能知道這玩意是咋用的,否則就成了“空談?wù)摺保ㄎ宜锞妥钣憛捒照勚耍。?。其他相關(guān)的技術(shù),感興趣的小伙伴可以關(guān)注底部Debug的技術(shù)公眾號(hào),或者加Debug的微信,拉你進(jìn)“微信版”的真正技術(shù)交流群!一起學(xué)習(xí)、共同成長(zhǎng)!
補(bǔ)充:
1、本文涉及到的相關(guān)的源代碼可以到此地址,check出來(lái)進(jìn)行查看學(xué)習(xí):
https://gitee.com/steadyjack/SpringBootTechnology
2、最近Debug發(fā)布了幾門(mén)重量級(jí)的課程,感興趣的小伙伴可以前往觀看學(xué)習(xí):
(1) 緩存中間件Redis技術(shù)入門(mén)與應(yīng)用場(chǎng)景實(shí)戰(zhàn)(SpringBoot2.x + 搶紅包系統(tǒng)設(shè)計(jì)與實(shí)戰(zhàn))
https://www.fightjava.com/web/index/course/detail/12
(2) 企業(yè)權(quán)限管理平臺(tái)(SpringBoot2.0+Shiro+Vue+Mybatis)
https://www.fightjava.com/web/index/course/detail/8