Java之Retry重試機制詳解

應用中需要實現(xiàn)一個功能唤殴,需要將數(shù)據(jù)上傳到遠程存儲服務斋攀,同時在返回成功情況下做其他操作。這個功能不復雜弧哎,分為兩個步驟雁比,第一步調(diào)用遠程rest服務上傳數(shù)據(jù)后對返回的結(jié)果進行處理;第二步拿到第一步結(jié)果或者異常撤嫩,如果出現(xiàn)錯誤或者異常則實現(xiàn)重試上傳邏輯偎捎,否則繼續(xù)接下來的業(yè)務操作。

常規(guī)解決方案


try-catch-redo簡單重試模式

在包裝正常上傳邏輯的基礎(chǔ)上,通過判斷返回結(jié)果或監(jiān)聽異常決定是否需要重試茴她,同時為了解決立即重試的無效性(假設異常是由外部不穩(wěn)定導致的:網(wǎng)絡抖動)寻拂,休眠一定延遲時間后重新執(zhí)行該邏輯功能。

    private void commonRetry() throws InterruptedException {

        Map<String, Object> paramMap = Maps.newHashMap();
        paramMap.put("tableName", "cretiveTable");
        paramMap.put("ds", "20160909");
        paramMap.put("dataMap", "data");
        paramMap.put("tableName", "cretiveTable");

        boolean result = false;
        try {
            result = uploadOdps(paramMap);
            if (!result) {
                Thread.sleep(1000);
                uploadOdps(paramMap);//單次重試
            }
        } catch (Exception e) {
            Thread.sleep(1000);
            uploadOdps(paramMap);//單次重試
        }
    }

try-catch-redo-retry strategy策略重試模式

上述方案還是有可能重試無效丈牢,解決這個問題嘗試增加重試次數(shù)retrycount以及重試間隔周期interval祭钉,達到增加重試有效的可能性。

 private void commonRetry() throws InterruptedException {

        Map<String, Object> paramMap = Maps.newHashMap();
        paramMap.put("tableName", "cretiveTable");
        paramMap.put("ds", "20160909");
        paramMap.put("dataMap", "data");
        paramMap.put("tableName", "cretiveTable");

        boolean result = false;
        try {
            result = uploadOdps(paramMap);
            if (!result) {
                Thread.sleep(1000);
                reUploadOdps(paramMap, 1000L, 3);//延遲多次重試
            }
        } catch (Exception e) {
            Thread.sleep(1000);
            reUploadOdps(paramMap, 1000L, 3);//延遲多次重試
        }
    }

方案一和方案二存在一個問題:正常邏輯和重試邏輯強耦合己沛,重試邏輯非常依賴正常邏輯的執(zhí)行結(jié)果慌核,對正常邏輯預期結(jié)果被動重試觸發(fā),對于重試根源往往由于邏輯復雜被淹沒申尼,可能導致后續(xù)運維對于重試邏輯要解決什么問題產(chǎn)生不一致理解垮卓。重試正確性難保證而且不利于運維,原因是重試設計依賴正常邏輯異尘фⅲ或重試根源的臆測扒接。

優(yōu)雅重試方案嘗試


應用命令設計模式解耦正常和重試邏輯

命令設計模式具體定義不展開闡述伪货,主要該方案看中命令模式能夠通過執(zhí)行對象完成接口操作邏輯们衙,同時內(nèi)部封裝處理重試邏輯,不暴露實現(xiàn)細節(jié)碱呼,對于調(diào)用者來看就是執(zhí)行了正常邏輯蒙挑,達到解耦的目標,具體看下功能實現(xiàn)愚臀。(類圖結(jié)構(gòu))

[圖片上傳失敗...(image-40ca90-1558507124456)]

而我們的調(diào)用者LogicClient無需關(guān)注重試忆蚀,通過重試者Retryer實現(xiàn)約定接口功能,同時 Retryer需要對重試邏輯做出響應和處理姑裂, Retryer具體重試處理又交給真正的IRtry接口的實現(xiàn)類OdpsRetry完成馋袜。通過采用命令模式,優(yōu)雅實現(xiàn)正常邏輯和重試邏輯分離舶斧,同時通過構(gòu)建重試者角色欣鳖,實現(xiàn)正常邏輯和重試邏輯的分離,讓重試有更好的擴展性茴厉。

使用Guava retryer優(yōu)雅的實現(xiàn)接口重調(diào)機制


Guava retryer工具與spring-retry類似泽台,都是通過定義重試者角色來包裝正常邏輯重試,但是Guava retryer有更優(yōu)的策略定義矾缓,在支持重試次數(shù)和重試頻度控制基礎(chǔ)上怀酷,能夠兼容支持多個異常或者自定義實體對象的重試源定義嗜闻,讓重試功能有更多的靈活性蜕依。Guava Retryer也是線程安全的,入口調(diào)用邏輯采用的是Java.util.concurrent.Callable的call方法。 使用Guava retryer 很簡單样眠,我們只要做以下幾步:

1竞滓、Maven POM 引入

<guava-retry.version>2.0.0</guava-retry.version>
<dependency>
      <groupId>com.github.rholder</groupId>
      <artifactId>guava-retrying</artifactId>
      <version>${guava-retry.version}</version>
</dependency>

2、定義實現(xiàn)Callable接口的方法吹缔,以便Guava retryer能夠調(diào)用

 private static Callable<Boolean> upload = new Callable<Boolean>() {
        @Override
        public Boolean call() throws Exception {
            String url = "test";
            String result = HttpMethod.POST(url, new ArrayList<BasicNameValuePair>());
            if (StringUtils.isEmpty(result)) {
                throw new RuntimeException("result is blank");
            }
            final JSONObject json = JSON.parseObject(result);
            if (json.getBoolean("result")) {
                return true;
            }
            return false;
        }
    };

3商佑、定義Retry對象并設置相關(guān)策略

Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
                //拋出runtime異常、checked異常時都會重試厢塘,但是拋出error不會重試茶没。
                .retryIfException()
                //返回false也需要重試
                .retryIfResult(Predicates.equalTo(false))
                //重調(diào)策略
                .withWaitStrategy(WaitStrategies.fixedWait(10, TimeUnit.SECONDS))
                //嘗試次數(shù)
                .withStopStrategy(StopStrategies.stopAfterAttempt(3))
                .build();
 
try {
    retryer.call(updateReimAgentsCall());
    # 以下方式可以不用實現(xiàn)第二步中所說的實現(xiàn)Callable接口定義方法
    //retry.call(() -> { FileUtils.downloadAttachment(projectNo, url, saveDir, fileName);  return true; });
} catch (ExecutionException e) {
    e.printStackTrace();
} catch (RetryException e) {
    logger.error("xxx");
}

簡單三步就能使用Guava Retryer優(yōu)雅的實現(xiàn)重調(diào)方法。

RetryerBuilder是一個Factory創(chuàng)建者晚碾,可以自定義設置重試源且支持多個重試源抓半,可以配置重試次數(shù)或重試超時時間,以及可以配置等待時間間隔格嘁,創(chuàng)建重試者Retryer實例笛求。 RetryerBuilder的重試源支持Exception異常對象自定義斷言對象,通過retryIfException 和retryIfResult設置糕簿,同時支持多個且能兼容探入。

  • retryIfException:拋出runtime異常、checked異常時都會重試懂诗,但是拋出error不會重試蜂嗽。
  • retryIfRuntimeException:只會在拋runtime異常的時候才重試,checked異常和error都不重試殃恒。
  • retryIfExceptionOfType:允許我們只在發(fā)生特定異常的時候才重試植旧,比如NullPointerException和IllegalStateException都屬于runtime異常,也包括自定義的error ?如:
# 只在拋出error重試
retryIfExceptionOfType(Error.class)     
# 只有出現(xiàn)指定的異常的時候才重試离唐,如:&emsp;&emsp;
retryIfExceptionOfType(IllegalStateException.class)  
retryIfExceptionOfType(NullPointerException.class)  
# 或者通過Predicate實現(xiàn)
retryIfException(Predicates.or(Predicates.instanceOf(NullPointerException.class),  
                Predicates.instanceOf(IllegalStateException.class))) 

retryIfResult可以指定你的Callable方法在返回值的時候進行重試病附,如

// 返回false重試 
retryIfResult(Predicates.equalTo(false))  
//以_error結(jié)尾才重試 
retryIfResult(Predicates.containsPattern("_error$"))  

當發(fā)生重試之后,假如我們需要做一些額外的處理動作亥鬓,比如發(fā)個告警郵件啥的完沪,那么可以使用RetryListener。每次重試之后贮竟,guava-retrying會自動回調(diào)我們注冊的監(jiān)聽丽焊。也可以注冊多個RetryListener,會按照注冊順序依次調(diào)用咕别。

import com.github.rholder.retry.Attempt;  
import com.github.rholder.retry.RetryListener;  
import java.util.concurrent.ExecutionException;  
  
public class MyRetryListener<Boolean> implements RetryListener {  
    @Override  
    public <Boolean> void onRetry(Attempt<Boolean> attempt) {  
        // 第幾次重試,(注意:第一次重試其實是第一次調(diào)用)  
        System.out.print("[retry]time=" + attempt.getAttemptNumber());  
        // 距離第一次重試的延遲  
        System.out.print(",delay=" + attempt.getDelaySinceFirstAttempt());  
        // 重試結(jié)果: 是異常終止, 還是正常返回  
        System.out.print(",hasException=" + attempt.hasException());  
        System.out.print(",hasResult=" + attempt.hasResult());  
        // 是什么原因?qū)е庐惓? 
        if (attempt.hasException()) {  
            System.out.print(",causeBy=" + attempt.getExceptionCause().toString());  
        } else {  
            // 正常返回時的結(jié)果  
            System.out.print(",result=" + attempt.getResult());  
        }  
  
        // bad practice: 增加了額外的異常處理代碼  
        try {  
            Boolean result = attempt.get();  
            System.out.print(",rude get=" + result);  
        } catch (ExecutionException e) {  
            System.err.println("this attempt produce exception." + e.getCause().toString());  
        }  
        System.out.println();  
    }  
} 

接下來在Retry對象中指定監(jiān)聽:withRetryListener(new MyRetryListener<>())

image.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末技健,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子惰拱,更是在濱河造成了極大的恐慌雌贱,老刑警劉巖啊送,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異欣孤,居然都是意外死亡馋没,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門降传,熙熙樓的掌柜王于貴愁眉苦臉地迎上來篷朵,“玉大人,你說我怎么就攤上這事婆排∩” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵段只,是天一觀的道長腮猖。 經(jīng)常有香客問我,道長赞枕,這世上最難降的妖魔是什么澈缺? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮炕婶,結(jié)果婚禮上姐赡,老公的妹妹穿的比我還像新娘。我一直安慰自己古话,他們只是感情好雏吭,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布锁施。 她就那樣靜靜地躺著陪踩,像睡著了一般。 火紅的嫁衣襯著肌膚如雪悉抵。 梳的紋絲不亂的頭發(fā)上肩狂,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天,我揣著相機與錄音姥饰,去河邊找鬼傻谁。 笑死,一個胖子當著我的面吹牛列粪,可吹牛的內(nèi)容都是我干的审磁。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼岂座,長吁一口氣:“原來是場噩夢啊……” “哼态蒂!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起费什,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤钾恢,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體瘩蚪,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡泉懦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了疹瘦。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片崩哩。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖言沐,靈堂內(nèi)的尸體忽然破棺而出琢锋,到底是詐尸還是另有隱情,我是刑警寧澤呢灶,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布吴超,位于F島的核電站,受9級特大地震影響鸯乃,放射性物質(zhì)發(fā)生泄漏鲸阻。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一缨睡、第九天 我趴在偏房一處隱蔽的房頂上張望鸟悴。 院中可真熱鬧,春花似錦奖年、人聲如沸细诸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽震贵。三九已至,卻和暖如春水评,著一層夾襖步出監(jiān)牢的瞬間猩系,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工中燥, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留寇甸,地道東北人。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓疗涉,卻偏偏與公主長得像拿霉,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子咱扣,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容