中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Java中重試機制的方式有哪些

發布時間:2023-03-14 16:44:11 來源:億速云 閱讀:93 作者:iii 欄目:開發技術

今天小編給大家分享一下Java中重試機制的方式有哪些的相關知識點,內容詳細,邏輯清晰,相信大部分人都還太了解這方面的知識,所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來了解一下吧。

重試機制在分布式系統中,或者調用外部接口中,都是十分重要的。

重試機制可以保護系統減少因網絡波動、依賴服務短暫性不可用帶來的影響,讓系統能更穩定的運行的一種保護機制。

為了方便說明,先假設我們想要進行重試的方法如下:

@Slf4j
@Component
public class HelloService {
 
    private static AtomicLong helloTimes = new AtomicLong();
 
    public String hello(){
        long times = helloTimes.incrementAndGet();
        if (times % 4 != 0){
            log.warn("發生異常,time:{}", LocalTime.now() );
            throw new HelloRetryException("發生Hello異常");
        }
        return "hello";
    }
}

調用處:

@Slf4j
@Service
public class HelloRetryService implements IHelloService{
 
    @Autowired
    private HelloService helloService;
 
    public String hello(){
        return helloService.hello();
    }
}

也就是說,這個接口每調4次才會成功一次。

1.手動重試

先來用最簡單的方法,直接在調用的時候進重試:

// 手動重試
public String hello(){
    int maxRetryTimes = 4;
    String s = "";
    for (int retry = 1; retry <= maxRetryTimes; retry++) {
        try {
            s = helloService.hello();
            log.info("helloService返回:{}", s);
            return s;
        } catch (HelloRetryException e) {
            log.info("helloService.hello() 調用失敗,準備重試");
        }
    }
    throw new HelloRetryException("重試次數耗盡");
}

發生異常,time:10:17:21.079413300
helloService.hello() 調用失敗,準備重試
發生異常,time:10:17:21.085861800
helloService.hello() 調用失敗,準備重試
發生異常,time:10:17:21.085861800
helloService.hello() 調用失敗,準備重試
helloService返回:hello
service.helloRetry():hello

程序在極短的時間內進行了4次重試,然后成功返回。

這樣雖然看起來可以解決問題,但實踐上,由于沒有重試間隔,很可能當時依賴的服務尚未從網絡異常中恢復過來,所以極有可能接下來的幾次調用都是失敗的。

而且,這樣需要對代碼進行大量的侵入式修改,顯然,不優雅。

2.代理模式

上面的處理方式由于需要對業務代碼進行大量修改,雖然實現了功能,但是對原有代碼的侵入性太強,可維護性差。

所以需要使用一種更優雅一點的方式,不直接修改業務代碼,那要怎么做呢?

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

@Slf4j
public class HelloRetryProxyService implements IHelloService{
   
    @Autowired
    private HelloRetryService helloRetryService;
    
    @Override
    public String hello() {
        int maxRetryTimes = 4;
        String s = "";
        for (int retry = 1; retry <= maxRetryTimes; retry++) {
            try {
                s = helloRetryService.hello();
                log.info("helloRetryService 返回:{}", s);
                return s;
            } catch (HelloRetryException e) {
                log.info("helloRetryService.hello() 調用失敗,準備重試");
            }
        }
        throw new HelloRetryException("重試次數耗盡");
    }
}

這樣,重試邏輯就都由代理類來完成,原業務類的邏輯就不需要修改了,以后想修改重試邏輯也只需要修改這個類就行了,分工明確。比如,現在想要在重試之間加上一個延遲,只需要做一點點修改即可:

@Override
public String hello() {
    int maxRetryTimes = 4;
    String s = "";
    for (int retry = 1; retry <= maxRetryTimes; retry++) {
        try {
            s = helloRetryService.hello();
            log.info("helloRetryService 返回:{}", s);
            return s;
        } catch (HelloRetryException e) {
            log.info("helloRetryService.hello() 調用失敗,準備重試");
        }
        // 延時一秒
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    throw new HelloRetryException("重試次數耗盡");
}

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

如果每個類都寫這么一長串類似的代碼,顯然,不優雅!

3.JDK動態代理

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

@Slf4j
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 = 0;
 
        while (times < RetryConstant.MAX_TIMES) {
            try {
                return method.invoke(subject, args);
            } catch (Exception e) {
                times++;
                log.info("times:{},time:{}", times, LocalTime.now());
                if (times >= RetryConstant.MAX_TIMES) {
                    throw new RuntimeException(e);
                }
            }
 
            // 延時一秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
 
        return null;
    }
 
    /**
     * 獲取動態代理
     *
     * @param realSubject 代理對象
     */
    public static Object getProxy(Object realSubject) {
        InvocationHandler handler = new RetryInvocationHandler(realSubject);
        return Proxy.newProxyInstance(handler.getClass().getClassLoader(),
                realSubject.getClass().getInterfaces(), handler);
    }
 
}

咱們測試一下:

@Test
public void helloDynamicProxy() {
    IHelloService realService = new HelloService();
    IHelloService proxyService = (IHelloService)RetryInvocationHandler.getProxy(realService);
 
    String hello = proxyService.hello();
    log.info("hello:{}", hello);
}

輸出結果如下:

hello times:1
發生異常,time:11:22:20.727586700
times:1,time:11:22:20.728083
hello times:2
發生異常,time:11:22:21.728858700
times:2,time:11:22:21.729343700
hello times:3
發生異常,time:11:22:22.729706600
times:3,time:11:22:22.729706600
hello times:4
hello:hello

在重試了4次之后輸出了Hello,符合預期。

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

不過不要高興的太早,這里因為被代理的HelloService是一個簡單的類,沒有依賴其它類,所以直接創建是沒有問題的,但如果被代理的類依賴了其它被Spring容器管理的類,則這種方式會拋出異常,因為沒有把被依賴的實例注入到創建的代理實例中。

這種情況下,就比較復雜了,需要從Spring容器中獲取已經裝配好的,需要被代理的實例,然后為其創建代理類實例,并交給Spring容器來管理,這樣就不用每次都重新創建新的代理類實例了。

話不多說,擼起袖子就是干。

新建一個工具類,用來獲取代理實例:

@Component
public class RetryProxyHandler {
 
    @Autowired
    private ConfigurableApplicationContext context;
 
    public Object getProxy(Class clazz) {
        // 1. 從Bean中獲取對象
        DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory)context.getAutowireCapableBeanFactory();
        Map<String, Object> beans = beanFactory.getBeansOfType(clazz);
        Set<Map.Entry<String, Object>> entries = beans.entrySet();
        if (entries.size() <= 0){
            throw new ProxyBeanNotFoundException();
        }
        // 如果有多個候選bean, 判斷其中是否有代理bean
        Object bean = null;
        if (entries.size() > 1){
            for (Map.Entry<String, Object> entry : entries) {
                if (entry.getKey().contains(PROXY_BEAN_SUFFIX)){
                    bean = entry.getValue();
                }
            };
            if (bean != null){
                return bean;
            }
            throw new ProxyBeanNotSingleException();
        }
 
        Object source = beans.entrySet().iterator().next().getValue();
        Object source = beans.entrySet().iterator().next().getValue();
 
        // 2. 判斷該對象的代理對象是否存在
        String proxyBeanName = clazz.getSimpleName() + PROXY_BEAN_SUFFIX;
        Boolean exist = beanFactory.containsBean(proxyBeanName);
        if (exist) {
            bean = beanFactory.getBean(proxyBeanName);
            return bean;
        }
 
        // 3. 不存在則生成代理對象
        bean = RetryInvocationHandler.getProxy(source);
 
        // 4. 將bean注入spring容器
        beanFactory.registerSingleton(proxyBeanName, bean);
        return bean;
    }
}

使用的是JDK動態代理:

@Slf4j
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 = 0;
 
        while (times < RetryConstant.MAX_TIMES) {
            try {
                return method.invoke(subject, args);
            } catch (Exception e) {
                times++;
                log.info("retry times:{},time:{}", times, LocalTime.now());
                if (times >= RetryConstant.MAX_TIMES) {
                    throw new RuntimeException(e);
                }
            }
 
            // 延時一秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
 
        return null;
    }
 
    /**
     * 獲取動態代理
     *
     * @param realSubject 代理對象
     */
    public static Object getProxy(Object realSubject) {
        InvocationHandler handler = new RetryInvocationHandler(realSubject);
        return Proxy.newProxyInstance(handler.getClass().getClassLoader(),
                realSubject.getClass().getInterfaces(), handler);
    }
 
}

至此,主要代碼就完成了,修改一下HelloService類,增加一個依賴:

@Slf4j
@Component
public class HelloService implements IHelloService{
 
    private static AtomicLong helloTimes = new AtomicLong();
 
    @Autowired
    private NameService nameService;
 
    public String hello(){
        long times = helloTimes.incrementAndGet();
        log.info("hello times:{}", times);
        if (times % 4 != 0){
            log.warn("發生異常,time:{}", LocalTime.now() );
            throw new HelloRetryException("發生Hello異常");
        }
        return "hello " + nameService.getName();
    }
}

NameService其實很簡單,創建的目的僅在于測試依賴注入的Bean能否正常運行。

@Service
public class NameService {
 
    public String getName(){
        return "Frank";
    }
}

測試一下:

@Test
public void helloJdkProxy() throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
    IHelloService proxy = (IHelloService) retryProxyHandler.getProxy(HelloService.class);
    String hello = proxy.hello();
    log.info("hello:{}", hello);
}

結果:

hello times:1
發生異常,time:14:40:27.540672200
retry times:1,time:14:40:27.541167400
hello times:2
發生異常,time:14:40:28.541584600
retry times:2,time:14:40:28.542033500
hello times:3
發生異常,time:14:40:29.542161500
retry times:3,time:14:40:29.542161500
hello times:4
hello:hello Frank

完美,這樣就不用擔心依賴注入的問題了,因為從Spring容器中拿到的Bean對象都是已經注入配置好的。當然,這里僅考慮了單例Bean的情況,可以考慮的更加完善一點,判斷一下容器中Bean的類型是Singleton還是Prototype,如果是Singleton則像上面這樣進行操作,如果是Prototype則每次都新建代理類對象。

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

4.Spring AOP

想要無侵入式的修改原有邏輯?想要一個注解就實現重試?用Spring AOP不就能完美實現嗎?使用AOP來為目標調用設置切面,即可在目標方法調用前后添加一些額外的邏輯。

先創建一個注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Retryable {
    int retryTimes() default 3;
    int retryInterval() default 1;
}

有兩個參數,retryTimes 代表最大重試次數,retryInterval代表重試間隔。

@Retryable(retryTimes = 4, retryInterval = 2)
public String hello(){
    long times = helloTimes.incrementAndGet();
    log.info("hello times:{}", times);
    if (times % 4 != 0){
        log.warn("發生異常,time:{}", LocalTime.now() );
        throw new HelloRetryException("發生Hello異常");
    }
    return "hello " + nameService.getName();
}

接著,進行最后一步,編寫AOP切面:

@Slf4j
@Aspect
@Component
public class RetryAspect {
 
    @Pointcut("@annotation(com.mfrank.springboot.retry.demo.annotation.Retryable)")
    private void retryMethodCall(){}
 
    @Around("retryMethodCall()")
    public Object retry(ProceedingJoinPoint joinPoint) throws InterruptedException {
        // 獲取重試次數和重試間隔
        Retryable retry = ((MethodSignature)joinPoint.getSignature()).getMethod().getAnnotation(Retryable.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 * 1000);
        }
        throw new RetryExhaustedException("重試次數耗盡", error);
    }
}

開始測試:

@Autowired
private HelloService helloService;
 
@Test
public void helloAOP(){
    String hello = helloService.hello();
    log.info("hello:{}", hello);
}

打印結果:

hello times:1
發生異常,time:16:49:30.224649800
調用發生異常,開始重試,retryTimes:1
hello times:2
發生異常,time:16:49:32.225230800
調用發生異常,開始重試,retryTimes:2
hello times:3
發生異常,time:16:49:34.225968900
調用發生異常,開始重試,retryTimes:3
hello times:4
hello:hello Frank

這樣就相當優雅了,一個注解就能搞定重試,簡直不要更棒。

5.Spring 的重試注解

實際上Spring中就有比較完善的重試機制,比上面的切面更加好用,還不需要自己動手重新造輪子。

那讓我們先來看看這個輪子究竟好不好使。

先引入重試所需的jar包:

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

然后在啟動類或者配置類上添加@EnableRetry注解,接下來在需要重試的方法上添加@Retryable注解(嗯?好像跟我自定義的注解一樣?竟然抄襲我的注解【手動狗頭】)

@Retryable
public String hello(){
    long times = helloTimes.incrementAndGet();
    log.info("hello times:{}", times);
    if (times % 4 != 0){
        log.warn("發生異常,time:{}", LocalTime.now() );
        throw new HelloRetryException("發生Hello異常");
    }
    return "hello " + nameService.getName();
}

默認情況下,會重試三次,重試間隔為1秒。當然我們也可以自定義重試次數和間隔。這樣就跟我前面實現的功能是一毛一樣的了。

但Spring里的重試機制還支持很多很有用的特性,比如說,可以指定只對特定類型的異常進行重試,這樣如果拋出的是其它類型的異常則不會進行重試,就可以對重試進行更細粒度的控制。默認為空,會對所有異常都重試。

@Retryable{value = {HelloRetryException.class}}
public String hello(){
    ...
}

也可以使用include和exclude來指定包含或者排除哪些異常進行重試。

可以用maxAttemps指定最大重試次數,默認為3次。

可以用interceptor設置重試攔截器的bean名稱。

可以通過label設置該重試的唯一標志,用于統計輸出。

可以使用exceptionExpression來添加異常表達式,在拋出異常后執行,以判斷后續是否進行重試。

此外,Spring中的重試機制還支持使用backoff來設置重試補償機制,可以設置重試間隔,并且支持設置重試延遲倍數。

舉個例子:

@Retryable(value = {HelloRetryException.class}, maxAttempts = 5,
           backoff = @Backoff(delay = 1000, multiplier = 2))
public String hello(){
    ...
}

該方法調用將會在拋出HelloRetryException異常后進行重試,最大重試次數為5,第一次重試間隔為1s,之后以2倍大小進行遞增,第二次重試間隔為2s,第三次為4s,第四次為8s。

重試機制還支持使用@Recover 注解來進行善后工作,當重試達到指定次數之后,將會調用該方法,可以在該方法中進行日志記錄等操作。

這里值得注意的是,想要@Recover 注解生效的話,需要跟被@Retryable 標記的方法在同一個類中,且被@Retryable 標記的方法不能有返回值,否則不會生效。

并且如果使用了@Recover注解的話,重試次數達到最大次數后,如果在@Recover標記的方法中無異常拋出,是不會拋出原異常的。

@Recover
public boolean recover(Exception e) {
    log.error("達到最大重試次數",e);
    return false;
}

除了使用注解外,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:組合重試策略,有兩種組合方式,樂觀組合重試策略是指只要有一個策略允許即可以重試,

悲觀組合重試策略是指只要有一個策略不允許即可以重試,但不管哪種組合方式,組合中的每一個策略都會執行

可以看出,Spring中的重試機制還是相當完善的,比上面自己寫的AOP切面功能更加強大。

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

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

以上就是“Java中重試機制的方式有哪些”這篇文章的所有內容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會為大家更新不同的知識,如果還想學習更多的知識,請關注億速云行業資訊頻道。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

信阳市| 鄢陵县| 五原县| 和田市| 卓尼县| 滨海县| 富顺县| 兴和县| 定西市| 桐乡市| 武隆县| 新河县| 勃利县| 黄冈市| 新巴尔虎左旗| 辉南县| 宣威市| 绥阳县| 陕西省| 搜索| 肥乡县| 姚安县| 凤冈县| 青岛市| 措勤县| 秦皇岛市| 盘锦市| 稻城县| 茂名市| 宿州市| 银川市| 德令哈市| 大姚县| 平度市| 甘德县| 大悟县| 马公市| 西青区| 汤原县| 凤城市| 赣榆县|