對於“接口/方法 重試”,相信很多小夥伴都聽說過,但是在實際項目中卻很少真正去實踐過,在本篇文章中,Debug將給各位小夥伴介紹一種“重試”機制的實現,即Guava_Retrying,相對於傳統的Spring_Retrying或者動態代理實現的重試功能而言,本文要介紹的Guava_Retrying機制使用起來將更加容易、靈活性更強!
老趙:“這個 接口/方法 調用又失敗了,老李啊,你去寫個重試功能吧!”。
老李:“他孃的,這接口調用咋又不行了。。。行吧,老子立馬給你擼一個重試功能” 。
這樣的對話,相信有些小夥伴會感覺似曾相識!特別是當自己在工位上安安靜靜的寫代碼時,會突然性的接到技術老大分配給自己的這種需求。。。沒啥好說的,只能潛下心,去研究研究了!
對於“重試”,那可是有場景限制的,不是什麼場景都適合重試,比如參數校驗不合法、寫操作等(因為要考慮到寫是否冪等)都不適合重試。
而諸如“遠程調用超時”、“網絡突然中斷”等業務場景則可以進行重試,在微服務 治理框架中,通常都有自己的重試與超時配置,比如Dubbo可以設置retries=1,timeout=500調用失敗只重試1次,超過500ms調用仍未返回則調用失敗(詳情可以觀看學習Debug錄製的“分佈式服務調度Dubbo實戰教程 http://www.fightjava.com/web/index/course/detail/2 ”)
對於“外部 RPC 調用”,或者“數據入庫”等操作,如果一次操作失敗,則可以進行多次重試,從而提高調用成功的可能性。
下面我們基於前面搭建的SpringBoot多模塊企業級項目,基於Guava_Retrying初步實現所謂的“重試功能”。工欲善其事必先利其器,首先當然是需要加入Guava_Retrying的依賴Jar,如下所示:
<dependency>
<groupid>com.github.rholder/<groupid>
<artifactid>guava-retrying/<artifactid>
<version>2.0.0/<version>
/<dependency>
之後,我們來寫個簡單的入門案例,先來 過一把“接口調用重試”的癮!
/**
* Guava_retrying機制實現重試
* @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("----重試時 變量i的疊加邏輯----");
return i++;
}
public static void main(String[] args) {
//TODO:定義任務實例
Callable<string> callable= () -> {
Integer res=execute();
//當重試達到3 + 1次之後 我們就不玩了
if (res>3){
return res.toString();
}
return null;
};
//TODO:定義重試器
Retryer<string> retryer=RetryerBuilder.<string>newBuilder()
//TODO:當返回結果為Null時 - 執行重試
.retryIfResult(Predicates.isNull())
//TODO:當執行核心業務邏輯拋出RuntimeException - 執行重試
.retryIfRuntimeException()
//TODO:還可以自定義拋出何種異常時 - 執行重試
.retryIfExceptionOfType(IOException.class)
.build();
try {
retryer.call(callable);
} catch (ExecutionException | RetryException e) {
e.printStackTrace();
}
}
}/<string>/<string>/<string>
運行該main方法,可以得到如下的結果:
從該上述代碼中,我們得知“重試機制”功能實現的核心在於定義Retryer實例以及Callable任務運行實例 ,特別是Retryer實例,可以設置“什麼時機重試”。
除此之外,對於 Retryer實例 我們還可以設置“重試的次數”、“重試的時間間隔”、“每次重試時,定義Listener監聽一些操作邏輯”等等。如下代碼所示:
public static void main(String[] args) {
//TODO:定義任務實例
Callable<string> callable= () -> {
return null;
};
//TODO:每次重試時 監聽器執行的邏輯
RetryListener retryListener=new RetryListener() {
@Override
publicvoid onRetry(Attempt /<string>attempt) {
Long curr=attempt.getAttemptNumber();
log.info("----每次重試時 監聽器執行的邏輯,當前已經是第 {} 次重試了----",curr);
if (curr == 3){
log.error("--重試次數已到,是不是得該執行一些補償邏輯,如發送短信、發送郵件...");
}
}
};
//TODO:定義重試器
Retryer<string> retryer=RetryerBuilder.<string>newBuilder()
//TODO:當返回結果為Null時 - 執行重試
.retryIfResult(Predicates.isNull())
//TODO:當執行核心業務邏輯拋出RuntimeException - 執行重試
.retryIfRuntimeException()
//TODO:還可以自定義拋出何種異常時 - 執行重試
.retryIfExceptionOfType(IOException.class)
//TODO:每次重試時的時間間隔為5s
.withWaitStrategy(WaitStrategies.fixedWait(5L, TimeUnit.SECONDS))
//TODO:重試次數為3次,3次之後就不重試了
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
//TODO:每次重試時定義一個監聽器listener,監聽器的邏輯可以是 "日誌記錄"、"做一些補償操作"...
.withRetryListener(retryListener)
.build();
try {
retryer.call(callable);
} catch (ExecutionException | RetryException e) {
e.printStackTrace();
}
}/<string>/<string>
其中,我們加入了“監聽器Listener”、“定義了重試次數”、“定義了每次重試的時間間隔”,這三個才是Guava_Retrying提供給開發者重量級的玩意,如下代碼所示!
//TODO:每次重試時的時間間隔為5s
.withWaitStrategy(WaitStrategies.fixedWait(5L, TimeUnit.SECONDS))
//TODO:重試次數為3次,3次之後就不重試了
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
//TODO:每次重試時定義一個監聽器listener,監聽器的邏輯可以是 "日誌記錄"、"做一些補償操作"...
.withRetryListener(retryListener)
其中,“重試次數策略StopStrategies”、“重試的時間間隔設置策略WaitStrategies”中Guava_Retrying提供了許多種選擇,比如“重試次數可以是一個隨機數”、“重試的時間間隔也可以設置為某個區間範圍內的隨機數”等等,下圖為運行結果截圖:
下面,我們來擼一個真實的業務場景,即“調用某個接口的方法,用於獲取SysConfig配置表中某個字典配置記錄,如果該字典配置記錄不存在(即返回Null),那我們就重試3次,如果期間獲取到了,那麼就返回結果;3次過後,依舊為Null時,則執行一些補償性的措施:即發送郵件通知給到指定的人員,讓他們上去檢查檢查相應的數據狀況!”
下圖為 系統字典配置表SysConfig存儲的字典記錄,其中,沒有id=11的記錄,我們將拿著這個 id=11 來進行測試:
如下代碼為正常項目開發過程中我們自定義的Service及其方法:
/**
* Guava_Retrying重試機制的 小型真實案例
* @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:獲取某個字典配置詳情
public SysConfig getConfigInfo(final Integer id){
SysConfig config=sysConfigMapper.selectByPrimaryKey(id);
if (config==null){
//TODO:當沒有查詢到該數據記錄時,執行重試邏輯
doRetry(id);
config=sysConfigMapper.selectByPrimaryKey(id);
}
return config;
}
//TODO:執行重試邏輯
private void doRetry(final Integer id){
//TODO:定義任務實例
Callable<sysconfig> callable= () -> {
return sysConfigMapper.selectByPrimaryKey(id);
};
//TODO:每次重試時 監聽器執行的邏輯
RetryListener retryListener=new RetryListener() {
@Override
publicvoid onRetry(Attempt /<sysconfig>attempt) {
Long curr=attempt.getAttemptNumber();
log.info("----每次重試時 監聽器執行的邏輯,當前已經是第 {} 次重試了----",curr);
//當達到3次時 就執行一些補償性的措施,如發送郵件通知某些大佬….
if (curr == 3){
log.error("--重試次數已到,是不是得該執行一些補償邏輯,如發送短信、發送郵件...");
emailSendService.sendSimpleEmail("重試次數已到","請各位大佬上去檢查一下sysConfig是否存在","[email protected]");
}
}
};
//TODO:定義重試器
Retryer<sysconfig> retryer= RetryerBuilder.<sysconfig>newBuilder()
//TODO:當返回結果為 false 時 - 執行重試(即sysCofig為null)
.retryIfResult(Objects::isNull)
//TODO:當執行核心業務邏輯拋出RuntimeException - 執行重試
.retryIfRuntimeException()
//TODO:還可以自定義拋出何種異常時 - 執行重試
.retryIfExceptionOfType(IOException.class)
//TODO:每次重試時的時間間隔為10s (當然啦,實際項目中一般是不超過1s的,如500ms,這裡是為了方便模擬演示)
.withWaitStrategy(WaitStrategies.fixedWait(10L, TimeUnit.SECONDS))
//TODO:重試次數為3次,3次之後就不重試了
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
//TODO:每次重試時定義一個監聽器listener,監聽器的邏輯可以是 "日誌記錄"、"做一些補償操作"...
.withRetryListener(retryListener)
.build();
try {
retryer.call(callable);
} catch (ExecutionException | RetryException e) {
e.printStackTrace();
}
}
}/<sysconfig>/<sysconfig>
最後寫個Java Unit Test,即Java單元測試案例,如下所示:
@Autowired
private RetryService retryService;
@Test
public void method8() throws Exception{
final Integer id=11;
SysConfig entity=retryService.getConfigInfo(id);
log.info("---結果:{}",entity);
}
點擊運行該單元測試案例,啥事都不要做,等待運行結果,你會發現“重試”的效果我們已經實現了!如下所示:
我們再點擊運行該單元測試案例,然後在它運行了第1次重試機會之後,我們趕緊手動到數據庫將 id=12 的那條系統配置記錄,調整為 id=11 !然後再來看運行的結果,如下圖所示:
如下圖為“補償性措施”中的“發送郵件”:
好了,本篇文章我們就介紹到這裡了,建議各位小夥伴一定要照著文章提供的樣例代碼擼一擼,只有擼過才能知道這玩意是咋用的,否則就成了“空談者”。其他相關的技術,感興趣的小夥伴可以關注Debug的技術公眾號,或者私信Debug!
閱讀更多 程序員實戰基地 的文章