Java重試機制


重試作用:

對於重試是有場景限制的,不是什么場景都適合重試,比如參數校驗不合法、寫操作等(要考慮寫是否冪等)都不適合重試。

遠程調用超時、網絡突然中斷可以重試。在微服務治理框架中,通常都有自己的重試與超時配置,比如dubbo可以設置retries=1,timeout=500調用失敗只重試1次,超過500ms調用仍未返回則調用失敗。

比如外部 RPC 調用,或者數據入庫等操作,如果一次操作失敗,可以進行多次重試,提高調用成功的可能性

優雅的重試機制要具備幾點:

  • 無侵入:這個好理解,不改動當前的業務邏輯,對於需要重試的地方,可以很簡單的實現
  • 可配置:包括重試次數,重試的間隔時間,是否使用異步方式等
  • 通用性:最好是無改動(或者很小改動)的支持絕大部分的場景,拿過來直接可用

優雅重試共性和原理:

  • 正常和重試優雅解耦,重試斷言條件實例或邏輯異常實例是兩者溝通的媒介。
  • 約定重試間隔,差異性重試策略,設置重試超時時間,進一步保證重試有效性以及重試流程穩定性。
  • 都使用了命令設計模式,通過委托重試對象完成相應的邏輯操作,同時內部封裝實現重試邏輯。
  • Spring-tryer和guava-tryer工具都是線程安全的重試,能夠支持並發業務場景的重試邏輯正確性。

優雅重試適用場景:

  • 功能邏輯中存在不穩定依賴場景,需要使用重試獲取預期結果或者嘗試重新執行邏輯不立即結束。比如遠程接口訪問,數據加載訪問,數據上傳校驗等等。
  • 對於異常場景存在需要重試場景,同時希望把正常邏輯和重試邏輯解耦。
  • 對於需要基於數據媒介交互,希望通過重試輪詢檢測執行邏輯場景也可以考慮重試方案。 

優雅重試解決思路:

切面方式

這個思路比較清晰,在需要添加重試的方法上添加一個用於重試的自定義注解,然后在切面中實現重試的邏輯,主要的配置參數則根據注解中的選項來初始化

優點:

    • 真正的無侵入

缺點:

    • 某些方法無法被切面攔截的場景無法覆蓋(如spring-aop無法切私有方法,final方法)
    • 直接使用aspecj則有些小復雜;如果用spring-aop,則只能切被spring容器管理的bean

消息總線方式

這個也比較容易理解,在需要重試的方法中,發送一個消息,並將業務邏輯作為回調方法傳入;由一個訂閱了重試消息的consumer來執行重試的業務邏輯

優點:

    • 重試機制不受任何限制,即在任何地方你都可以使用
    • 利用EventBus框架,可以非常容易把框架搭起來

缺點:

    • 業務侵入,需要在重試的業務處,主動發起一條重試消息
    • 調試理解復雜(消息總線方式的最大優點和缺點,就是過於靈活了,你可能都不知道什么地方處理這個消息,特別是新的童鞋來維護這段代碼時)
    • 如果要獲取返回結果,不太好處理, 上下文參數不好處理

模板方式

優點:

    • 簡單(依賴簡單:引入一個類就可以了; 使用簡單:實現抽象類,講業務邏輯填充即可;)
    • 靈活(這個是真正的靈活了,你想怎么干都可以,完全由你控制)

缺點:

    • 強侵入
    • 代碼臃腫

把這個單獨撈出來,主要是某些時候我就一兩個地方要用到重試,簡單的實現下就好了,也沒有必用用到上面這么重的方式;而且我希望可以針對代碼快進行重試

這個的設計還是非常簡單的,基本上代碼都可以直接貼出來,一目了然:

public abstract class RetryTemplate {

    private static final int DEFAULT_RETRY_TIME = 1;
    private int retryTime = DEFAULT_RETRY_TIME; 
    private int sleepTime = 0;// 重試的睡眠時間

    public int getSleepTime() {
        return sleepTime;
    }

    public RetryTemplate setSleepTime(int sleepTime) {
        if(sleepTime < 0) {
            throw new IllegalArgumentException("sleepTime should equal or bigger than 0");
        }
        this.sleepTime = sleepTime;
        return this;
    }

    public int getRetryTime() {
        return retryTime;
    }

    public RetryTemplate setRetryTime(int retryTime) {
        if (retryTime <= 0) {
            throw new IllegalArgumentException("retryTime should bigger than 0");
        }
        this.retryTime = retryTime;
        return this;
    }

    /**
     * 重試的業務執行代碼
     * 失敗時請拋出一個異常
     *
     * todo 確定返回的封裝類,根據返回結果的狀態來判定是否需要重試
     *
     * @return
     */
    protected abstract Object doBiz() throws Exception; //預留一個doBiz方法由業務方來實現,在其中書寫需要重試的業務代碼,然后執行即可

    public Object execute() throws InterruptedException {
        for (int i = 0; i < retryTime; i++) {
            try {
                return doBiz();
            } catch (Exception e) {
                log.error("業務執行出現異常,e: {}", e);
                Thread.sleep(sleepTime);
            }
        }
        return null;
    }

    public Object submit(ExecutorService executorService) {
        if (executorService == null) {
            throw new IllegalArgumentException("please choose executorService!");
        }
        return executorService.submit((Callable) () -> execute());
    }
}

使用示例:

public void retryDemo() throws InterruptedException {
    Object ans = new RetryTemplate() {
        @Override
        protected Object doBiz() throws Exception {
            int temp = (int) (Math.random() * 10);
            System.out.println(temp);
            if (temp > 3) {
                throw new Exception("generate value bigger then 3! need retry");
            }
            return temp;
        }
    }.setRetryTime(10).setSleepTime(10).execute();
    System.out.println(ans);
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM