Java 中的 7 種重試機制,還有誰不會?!

来源:https://www.cnblogs.com/javastack/archive/2023/08/09/17615979.html
-Advertisement-
Play Games

隨著互聯網的發展項目中的業務功能越來越複雜,有一些基礎服務我們不可避免的會去調用一些第三方的介面或者公司內其他項目中提供的服務,但是遠程服務的健壯性和網路穩定性都是不可控因素。 在測試階段可能沒有什麼異常情況,但上線後可能會出現調用的介面因為內部錯誤或者網路波動而出錯或返回系統異常,因此我們必須考慮 ...


隨著互聯網的發展項目中的業務功能越來越複雜,有一些基礎服務我們不可避免的會去調用一些第三方的介面或者公司內其他項目中提供的服務,但是遠程服務的健壯性和網路穩定性都是不可控因素。

在測試階段可能沒有什麼異常情況,但上線後可能會出現調用的介面因為內部錯誤或者網路波動而出錯或返回系統異常,因此我們必須考慮加上重試機制

重試機制 可以提高系統的健壯性,並且減少因網路波動依賴服務臨時不可用帶來的影響,讓系統能更穩定的運行。

1. 手動重試

手動重試:使用 while 語句進行重試:

@Service
public class OrderServiceImpl implements OrderService {
 public void addOrder() {
     int times = 1;
     while (times <= 5) {
         try {
             // 故意拋異常
             int i = 3 / 0;
             // addOrder
         } catch (Exception e) {
             System.out.println("重試" + times + "次");
             Thread.sleep(2000);
             times++;
             if (times > 5) {
                 throw new RuntimeException("不再重試!");
             }
         }
     }
 }
}

運行上述代碼:

上述代碼看上去可以解決重試問題,但實際上存在一些弊端:

  • 由於沒有重試間隔,很可能遠程調用的服務還沒有從網路異常中恢復,所以有可能接下來的幾次調用都會失敗
  • 代碼侵入式太高,調用方代碼不夠優雅
  • 項目中遠程調用的服務可能有很多,每個都去添加重試會出現大量的重覆代碼

推薦一個開源免費的 Spring Boot 實戰項目:

https://github.com/javastacks/spring-boot-best-practice

2. 靜態代理

上面的處理方式由於需要對業務代碼進行大量修改,雖然實現了功能,但是對原有代碼的侵入性太強,可維護性差。所以需要使用一種更優雅一點的方式,不直接修改業務代碼,那要怎麼做呢?

其實很簡單,直接在業務代碼的外面再包一層就行了,代理模式在這裡就有用武之地了。

@Service
public class OrderServiceProxyImpl implements OrderService {
    
    @Autowired
    private OrderServiceImpl orderService;

    @Override
    public void addOrder() {
        int times = 1;
        while (times <= 5) {
            try {
                // 故意拋異常
                int i = 3 / 0;
                orderService.addOrder();
            } catch (Exception e) {
                System.out.println("重試" + times + "次");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException ex) {
                    ex.printStackTrace();
                }
                times++;
                if (times > 5) {
                    throw new RuntimeException("不再重試!");
                }
            }
        }
        
    }
}

這樣,重試邏輯就都由代理類來完成,原業務類的邏輯就不需要修改了,以後想修改重試邏輯也只需要修改這個類就行了

代理模式雖然要更加優雅,但是如果依賴的服務很多的時候,要為每個服務都創建一個代理類,顯然過於麻煩,而且其實重試的邏輯都大同小異,無非就是重試的次數和延時不一樣而已。如果每個類都寫這麼一長串類似的代碼,顯然,不優雅!

3. JDK 動態代理

這時候,動態代理就閃亮登場了。只需要寫一個代理處理類就 ok 了

public class RetryInvocationHandler implements InvocationHandler {

    private final Object subject;

    public RetryInvocationHandler(Object subject) {
        this.subject = subject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        int times = 1;
        while (times <= 5) {
            try {
                // 故意拋異常
                int i = 3 / 0;
                return method.invoke(subject, args);
            } catch (Exception e) {
                System.out.println("重試【" + times + "】次");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException ex) {
                    ex.printStackTrace();
                }
                times++;
                if (times > 5) {
                    throw new RuntimeException("不再重試!");
                }
            }
        }
        return null;
    }

    public static Object getProxy(Object realSubject) {
        InvocationHandler handler = new RetryInvocationHandler(realSubject);
        return Proxy.newProxyInstance(handler.getClass().getClassLoader(), realSubject.getClass().getInterfaces(), handler);
    }

}

測試:

@RestController
@RequestMapping("/order")
public class OrderController {

    @Qualifier("orderServiceImpl")
    @Autowired
    private OrderService orderService;

    @GetMapping("/addOrder")
    public String addOrder() {
        OrderService orderServiceProxy = (OrderService)RetryInvocationHandler.getProxy(orderService);
        orderServiceProxy.addOrder();
        return "addOrder";
    }
    
}

動態代理可以將重試邏輯都放到一塊,顯然比直接使用代理類要方便很多,也更加優雅。

這裡使用的是JDK動態代理,因此就存在一個天然的缺陷,如果想要被代理的類,沒有實現任何介面,那麼就無法為其創建代理對象,這種方式就行不通了

4. CGLib 動態代理

既然已經說到了 JDK 動態代理,那就不得不提 CGLib 動態代理了。使用 JDK 動態代理對被代理的類有要求,不是所有的類都能被代理,而 CGLib 動態代理則剛好解決了這個問題

@Component
public class CGLibRetryProxyHandler implements MethodInterceptor {

    private Object target;

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        int times = 1;
        while (times <= 5) {
            try {
                // 故意拋異常
                int i = 3 / 0;
                return method.invoke(target, objects);
            } catch (Exception e) {
                System.out.println("重試【" + times + "】次");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException ex) {
                    ex.printStackTrace();
                }
                times++;
                if (times > 5) {
                    throw new RuntimeException("不再重試!");
                }
            }
        }
        return null;
    }

    public Object getCglibProxy(Object objectTarget){
        this.target = objectTarget;
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(objectTarget.getClass());
        enhancer.setCallback(this);
        Object result = enhancer.create();
        return result;
    }

}

測試:

@GetMapping("/addOrder")
public String addOrder() {
    OrderService orderServiceProxy = (OrderService) cgLibRetryProxyHandler.getCglibProxy(orderService);
    orderServiceProxy.addOrder();
    return "addOrder";
}

這樣就很棒了,完美的解決了 JDK 動態代理帶來的缺陷。優雅指數上漲了不少。

但這個方案仍舊存在一個問題,那就是需要對原來的邏輯進行侵入式修改,在每個被代理實例被調用的地方都需要進行調整,這樣仍然會對原有代碼帶來較多修改

5. 手動 Aop

考慮到以後可能會有很多的方法也需要重試功能,咱們可以將重試這個共性功能通過 AOP 來實現,使用 AOP 來為目標調用設置切麵,即可在目標方法調用前後添加一些重試的邏輯

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>

自定義註解:

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyRetryable {
    
    // 最大重試次數
    int retryTimes() default 3;
    // 重試間隔
    int retryInterval() default 1;

}
@Slf4j
@Aspect
@Component
public class RetryAspect {

    @Pointcut("@annotation(com.hcr.sbes.retry.annotation.MyRetryable)")
    private void retryMethodCall(){}

    @Around("retryMethodCall()")
    public Object retry(ProceedingJoinPoint joinPoint) throws InterruptedException {
        // 獲取重試次數和重試間隔
        MyRetryable retry = ((MethodSignature)joinPoint.getSignature()).getMethod().getAnnotation(MyRetryable.class);
        int maxRetryTimes = retry.retryTimes();
        int retryInterval = retry.retryInterval();

        Throwable error = new RuntimeException();
        for (int retryTimes = 1; retryTimes <= maxRetryTimes; retryTimes++){
            try {
                Object result = joinPoint.proceed();
                return result;
            } catch (Throwable throwable) {
                error = throwable;
                log.warn("調用發生異常,開始重試,retryTimes:{}", retryTimes);
            }
            Thread.sleep(retryInterval * 1000L);
        }
        throw new RuntimeException("重試次數耗盡", error);
    }

}

給需要重試的方法添加註解 @MyRetryable

@Service
public class OrderServiceImpl implements OrderService {

    @Override
    @MyRetryable(retryTimes = 5, retryInterval = 2)
    public void addOrder() {
        int i = 3 / 0;
        // addOrder
    }
    
}

這樣即不用編寫重覆代碼,實現上也比較優雅了:一個註解就實現重試。

6. spring-retry

Spring Boot 基礎就不介紹了,推薦看這個實戰項目:

https://github.com/javastacks/spring-boot-best-practice

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>

開啟重試功能:在啟動類或者配置類上添加 @EnableRetry 註解

在需要重試的方法上添加 @Retryable 註解

@Slf4j
@Service
public class OrderServiceImpl implements OrderService {

    @Override
    @Retryable(maxAttempts = 3, backoff = @Backoff(delay = 2000, multiplier = 2))
    public void addOrder() {
        System.out.println("重試...");
        int i = 3 / 0;
        // addOrder
    }

    @Recover
    public void recover(RuntimeException e) {
        log.error("達到最大重試次數", e);
    }
    
}

該方法調用後會進行重試,最大重試次數為 3,第一次重試間隔為 2s,之後以 2 倍大小進行遞增,第二次重試間隔為 4 s,第三次為 8s

Spring 的重試機制還支持很多很有用的特性,由三個註解完成:

  • @Retryable
  • @Backoff
  • @Recover

查看 @Retryable 註解源碼:指定異常重試、次數

public @interface Retryable {

 // 設置重試攔截器的 bean 名稱
    String interceptor() default "";
 
 // 只對特定類型的異常進行重試。預設:所有異常
    Class<? extends Throwable>[] value() default {};
 
 // 包含或者排除哪些異常進行重試
    Class<? extends Throwable>[] include() default {};
    Class<? extends Throwable>[] exclude() default {};
 
 // l設置該重試的唯一標誌,用於統計輸出
    String label() default "";

    boolean stateful() default false;
 
 // 最大重試次數,預設為 3 次
    int maxAttempts() default 3;
 
 
    String maxAttemptsExpression() default "";
 
 // 設置重試補償機制,可以設置重試間隔,並且支持設置重試延遲倍數
    Backoff backoff() default @Backoff;
 
 // 異常表達式,在拋出異常後執行,以判斷後續是否進行重試
    String exceptionExpression() default "";

    String[] listeners() default {};
}

@Backoff 註解: 指定重試回退策略(如果因為網路波動導致調用失敗,立即重試可能還是會失敗,最優選擇是等待一小會兒再重試。決定等待多久之後再重試的方法。通俗的說,就是每次重試是立即重試還是等待一段時間後重試)

@Recover 註解: 進行善後工作:當重試達到指定次數之後,會調用指定的方法來進行日誌記錄等操作

註意:
  • @Recover 註解標記的方法必須和被 @Retryable 標記的方法在同一個類中
  • 重試方法拋出的異常類型需要與 recover() 方法參數類型保持一致
  • recover() 方法返回值需要與重試方法返回值保證一致
  • recover() 方法中不能再拋出 Exception,否則會報無法識別該異常的錯誤

這裡還需要再提醒的一點是,由於 Spring Retry 用到了 Aspect 增強,所以就會有使用 Aspect 不可避免的坑——方法內部調用,如果被 @Retryable 註解的方法的調用方和被調用方處於同一個類中,那麼重試將會失效

通過以上幾個簡單的配置,可以看到 Spring Retry 重試機制考慮的比較完善,比自己寫AOP實現要強大很多

弊端:

但也還是存在一定的不足,Spring的重試機制只支持對 異常 進行捕獲,而無法對返回值進行校驗

@Retryable
public String hello() {
    long current = count.incrementAndGet();
    System.out.println("第" + current +"次被調用");
    if (current % 3 != 0) {
        log.warn("調用失敗");
        return "error";
    }
    return "success";
}

因此就算在方法上添加 @Retryable,也無法實現失敗重試

除了使用註解外,Spring Retry 也支持直接在調用時使用代碼進行重試:

@Test
public void normalSpringRetry() {
    // 表示哪些異常需要重試,key表示異常的位元組碼,value為true表示需要重試
    Map<Class<? extends Throwable>, Boolean> exceptionMap = new HashMap<>();
    exceptionMap.put(HelloRetryException.class, true);
 
    // 構建重試模板實例
    RetryTemplate retryTemplate = new RetryTemplate();
 
    // 設置重試回退操作策略,主要設置重試間隔時間
    FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
    long fixedPeriodTime = 1000L;
    backOffPolicy.setBackOffPeriod(fixedPeriodTime);
 
    // 設置重試策略,主要設置重試次數
    int maxRetryTimes = 3;
    SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(maxRetryTimes, exceptionMap);
 
    retryTemplate.setRetryPolicy(retryPolicy);
    retryTemplate.setBackOffPolicy(backOffPolicy);
 
    Boolean execute = retryTemplate.execute(
        //RetryCallback
        retryContext -> {
            String hello = helloService.hello();
            log.info("調用的結果:{}", hello);
            return true;
        },
        // RecoverCallBack
        retryContext -> {
            //RecoveryCallback
            log.info("已達到最大重試次數");
            return false;
        }
    );
}

此時唯一的好處是可以設置多種重試策略:

  • NeverRetryPolicy:只允許調用RetryCallback一次,不允許重試
  • AlwaysRetryPolicy:允許無限重試,直到成功,此方式邏輯不當會導致死迴圈
  • SimpleRetryPolicy:固定次數重試策略,預設重試最大次數為3次,RetryTemplate預設使用的策略
  • TimeoutRetryPolicy:超時時間重試策略,預設超時時間為1秒,在指定的超時時間內允許重試
  • ExceptionClassifierRetryPolicy:設置不同異常的重試策略,類似組合重試策略,區別在於這裡只區分不同異常的重試
  • CircuitBreakerRetryPolicy:有熔斷功能的重試策略,需設置3個參數openTimeout、resetTimeout和delegate
  • CompositeRetryPolicy:組合重試策略,有兩種組合方式,樂觀組合重試策略是指只要有一個策略允許即可以重試,悲觀組合重試策略是指只要有一個策略不允許即可以重試,但不管哪種組合方式,組合中的每一個策略都會執行

7. guava-retry

和 Spring Retry 相比,Guava Retry 具有更強的靈活性,並且能夠根據 返回值 來判斷是否需要重試

<dependency>
    <groupId>com.github.rholder</groupId>
    <artifactId>guava-retrying</artifactId>
    <version>2.0.0</version>
</dependency>
@Override
public String guavaRetry(Integer num) {
    Retryer<String> retryer = RetryerBuilder.<String>newBuilder()
            //無論出現什麼異常,都進行重試
            .retryIfException()
            //返回結果為 error時,進行重試
            .retryIfResult(result -> Objects.equals(result, "error"))
            //重試等待策略:等待 2s 後再進行重試
            .withWaitStrategy(WaitStrategies.fixedWait(2, TimeUnit.SECONDS))
            //重試停止策略:重試達到 3 次
            .withStopStrategy(StopStrategies.stopAfterAttempt(3))
            .withRetryListener(new RetryListener() {
                @Override
                public <V> void onRetry(Attempt<V> attempt) {
                    System.out.println("RetryListener: 第" + attempt.getAttemptNumber() + "次調用");
                }
            })
            .build();
    try {
        retryer.call(() -> testGuavaRetry(num));
    } catch (Exception e) {
        e.printStackTrace();
    }
    return "test";
}

先創建一個Retryer實例,然後使用這個實例對需要重試的方法進行調用,可以通過很多方法來設置重試機制:

  • retryIfException():對所有異常進行重試
  • retryIfRuntimeException():設置對指定異常進行重試
  • retryIfExceptionOfType():對所有 RuntimeException 進行重試
  • retryIfResult():對不符合預期的返回結果進行重試

還有五個以 withXxx 開頭的方法,用來對重試策略/等待策略/阻塞策略/單次任務執行時間限制/自定義監聽器進行設置,以實現更加強大的異常處理:

  • withRetryListener():設置重試監聽器,用來執行額外的處理工作
  • withWaitStrategy():重試等待策略
  • withStopStrategy():停止重試策略
  • withAttemptTimeLimiter:設置任務單次執行的時間限制,如果超時則拋出異常
  • withBlockStrategy():設置任務阻塞策略,即可以設置當前重試完成,下次重試開始前的這段時間做什麼事情

總結

從手動重試,到使用 Spring AOP 自己動手實現,再到站在巨人肩上使用特別優秀的開源實現 Spring Retry 和 Google guava-retrying,經過對各種重試實現方式的介紹,可以看到以上幾種方式基本上已經滿足大部分場景的需要:

  • 如果是基於 Spring 的項目,使用 Spring Retry 的註解方式已經可以解決大部分問題
  • 如果項目沒有使用 Spring 相關框架,則適合使用 Google guava-retrying:自成體系,使用起來更加靈活強大

版權聲明:本文為CSDN博主「sco5282」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。原文鏈接:https://blog.csdn.net/sco5282/article/details/131390099

近期熱文推薦:

1.1,000+ 道 Java面試題及答案整理(2022最新版)

2.勁爆!Java 協程要來了。。。

3.Spring Boot 2.x 教程,太全了!

4.別再寫滿屏的爆爆爆炸類了,試試裝飾器模式,這才是優雅的方式!!

5.《Java開發手冊(嵩山版)》最新發佈,速速下載!

覺得不錯,別忘了隨手點贊+轉發哦!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1.前言 相信許多開發同學看過《深入理解java虛擬機》,也閱讀過java虛擬機規範,書籍和文檔給人的感覺不夠直觀,本文從一個簡單的例子來看看jvm是如何工作的吧。 本文所有操作均在mac上進行。 2.示例代碼 示例代碼採用最常見的雙重檢索單例模式: package interview.desgin ...
  • 在 Python 中,迭代器是一個實現了 `__iter__` 和 `__next__` 方法的對象。`__iter__` 方法返回迭代器對象自身,而 `__next__` 方法返回下一個元素。換句話說,迭代器是一個可以逐個返回元素的對象。 下麵是一個簡單的迭代器示例,演示瞭如何實現 `__iter ...
  • Go語言的泛型是在Go 1.18版本中引入的一個新特性,它允許開發者編寫可以處理不同數據類型的代碼,而無需為每種數據類型都編寫重覆的代碼。以下是關於Go語言泛型的一些關鍵點: 1. 泛型是通過在函數或類型定義中使用類型參數來實現的。類型參數可以被看作是一個特殊的類型,它可以在函數或類型定義中的任何位 ...
  • python 是一種高級、面向對象、通用的編程語言,由`Guido van Rossum`發明,於1991年首次發佈。python 的設計哲學強調代碼的可讀性和簡潔性,同時也非常適合於大型項目的開發。python 語言被廣泛用於Web開發、科學計算、人工智慧、自動化測試、游戲開發等各個領域,並且擁有... ...
  • Hibernate 是一個開源的 ORM(對象關係映射)框架,它可以將 Java 對象與資料庫表進行映射,從而實現面向對象的數據持久化。使用 Hibernate,可以避免手動編寫 SQL 語句,從而提高開發效率,並且可以輕鬆地切換不同的資料庫。 ## 基礎概念 entity 實體類是映射到資料庫表中 ...
  • 用PHP封裝一個強大且通用的cURL方法。 用PHP封裝一個強大且通用的cURL方法。 用PHP封裝一個強大且通用的cURL方法。 用PHP封裝一個強大且通用的cURL方法。 ```php /** * @function 強大且通用的cURL請求庫 * @param $url string 路徑 如 ...
  • AbstractRoutingDataSource是Spring框架中的一個抽象類,可以實現多數據源的動態切換和路由,以滿足複雜的業務需求和提高系統的性能、可擴展性、靈活性。 ...
  • 源碼請到:自然語言處理練習: 學習自然語言處理時候寫的一些代碼 (gitee.com) 數據來源:norvig.com/big.txt 貝葉斯原理可看這裡:機器學習演算法學習筆記 - 過客匆匆,沉沉浮浮 - 博客園 (cnblogs.com) 一、數據預處理 將輸入的數據全部變為小寫方便後續處理 de ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...