1. 程式人生 > >【最佳實踐】如何優雅的進行重試

【最佳實踐】如何優雅的進行重試

本文口味:冰鎮楊梅 預計閱讀:20分鐘

說明

最近公司在搞活動,需要依賴一個第三方介面,測試階段並沒有什麼異常狀況,但上線後發現依賴的介面有時候會因為內部錯誤而返回系統異常,雖然概率不大,但總因為這個而報警總是不好的,何況死信佇列的訊息還需要麻煩運維進行重新投遞,所以加上重試機制勢在必行。

重試機制可以保護系統減少因網路波動、依賴服務短暫性不可用帶來的影響,讓系統能更穩定的執行的一種保護機制。讓你原本就穩如狗的系統更是穩上加穩。

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

@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次才會成功一次。

手動重試

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

// 手動重試
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次重試,然後成功返回。

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

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

代理模式

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

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

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

@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("重試次數耗盡");
}

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

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("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動態代理,因此就存在一個天然的缺陷,如果想要被代理的類,沒有實現任何介面,那麼就無法為其建立代理物件,這種方式就行不通了。

CGLib 動態代理

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

建立一個CGLib動態代理類:

@Slf4j
public class CGLibRetryProxyHandler implements MethodInterceptor {
    private Object target;//需要代理的目標物件

    //重寫攔截方法
    @Override
    public Object intercept(Object obj, Method method, Object[] arr, MethodProxy proxy) throws Throwable {
        int times = 0;

        while (times < RetryConstant.MAX_TIMES) {
            try {
                return method.invoke(target, arr);
            } catch (Exception e) {
                times++;
                log.info("cglib retry :{},time:{}", times, LocalTime.now());
                if (times >= RetryConstant.MAX_TIMES) {
                    throw new RuntimeException(e);
                }
            }

            // 延時一秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        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;
    }
}

想要換用CGLib動態代理,替換一下這兩行程式碼即可:

// 3. 不存在則生成代理物件
//        bean = RetryInvocationHandler.getProxy(source);
CGLibRetryProxyHandler proxyHandler = new CGLibRetryProxyHandler();
bean = proxyHandler.getCglibProxy(source);

開始測試:

@Test
public void helloCGLibProxy() {
    IHelloService proxy = (IHelloService) retryProxyHandler.getProxy(HelloService.class);
    String hello = proxy.hello();
    log.info("hello:{}", hello);

    hello = proxy.hello();
    log.info("hello:{}", hello);
}
hello times:1
發生異常,time:15:06:00.799679100
cglib retry :1,time:15:06:00.800175400
hello times:2
發生異常,time:15:06:01.800848600
cglib retry :2,time:15:06:01.801343100
hello times:3
發生異常,time:15:06:02.802180
cglib retry :3,time:15:06:02.802180
hello times:4
hello:hello Frank
hello times:5
發生異常,time:15:06:03.803933800
cglib retry :1,time:15:06:03.803933800
hello times:6
發生異常,time:15:06:04.804945400
cglib retry :2,time:15:06:04.805442
hello times:7
發生異常,time:15:06:05.806886500
cglib retry :3,time:15:06:05.807881300
hello times:8
hello:hello Frank

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

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

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

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

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(){2
    ...
}

也可以使用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不可避免的坑——方法內部呼叫,如果被 @Retryable 註解的方法的呼叫方和被呼叫方處於同一個類中,那麼重試將會失效。

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

Guava Retry

最後,再介紹另一個重試利器——Guava Retry。

相比Spring Retry,Guava Retry具有更強的靈活性,可以根據返回值校驗來判斷是否需要進行重試。

先來看一個小栗子:

先引入jar包:

<dependency>
    <groupId>com.github.rholder</groupId>
    <artifactId>guava-retrying</artifactId>
    <version>2.0.0</version>
</dependency>

然後用一個小Demo來感受一下:

@Test
public void guavaRetry() {
    Retryer<String> retryer = RetryerBuilder.<String>newBuilder()
        .retryIfExceptionOfType(HelloRetryException.class)
        .retryIfResult(StringUtils::isEmpty)
        .withWaitStrategy(WaitStrategies.fixedWait(3, TimeUnit.SECONDS))
        .withStopStrategy(StopStrategies.stopAfterAttempt(3))
        .build();

    try {
        retryer.call(() -> helloService.hello());
    } catch (Exception e){
        e.printStackTrace();
    }
}

先建立一個Retryer例項,然後使用這個例項對需要重試的方法進行呼叫,可以通過很多方法來設定重試機制,比如使用retryIfException來對所有異常進行重試,使用retryIfExceptionOfType方法來設定對指定異常進行重試,使用retryIfResult來對不符合預期的返回結果進行重試,使用retryIfRuntimeException方法來對所有RuntimeException進行重試。

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

通過跟Spring AOP的結合,可以實現比Spring Retry更加強大的重試功能。

仔細對比之下,Guava Retry可以提供的特性有:

  1. 可以設定任務單次執行的時間限制,如果超時則丟擲異常。
  2. 可以設定重試監聽器,用來執行額外的處理工作。
  3. 可以設定任務阻塞策略,即可以設定當前重試完成,下次重試開始前的這段時間做什麼事情。
  4. 可以通過停止重試策略和等待策略結合使用來設定更加靈活的策略,比如指數等待時長並最多10次呼叫,隨機等待時長並永不停止等等。

總結

本文由淺入深的對多種重試的姿勢進行了360度無死角教學,從最簡單的手動重試,到使用靜態代理,再到JDK動態代理和CGLib動態代理,再到Spring AOP,都是手工造輪子的過程,最後介紹了兩種目前比較好用的輪子,一個是Spring Retry,使用起來簡單粗暴,與Spring框架天生搭配,一個註解搞定所有事情,另一個便是Guava Retry,不依賴於Spring框架,自成體系,使用起來更加靈活強大。

個人認為,大部分場景下,Spring Retry提供的重試機制已經足夠強大,如果不需要Guava Retry提供的額外靈活性,使用Spring Retry就很棒了。當然,具體情況具體分析,但沒有必要的情況下,不鼓勵重複造輪子,先把別人的輪子研究清楚再想想還用不用自己動手。

本文到此就告一段落了,又用了一天的時間完成了完成了一篇文章,寫作的目的在於總結和分享,我相信最佳實踐是可以總結和積累下來的,在大多數場景下都是適用的,這些最佳實踐會在逐漸的積累過程中,成為比經驗更為重要的東西。因為經驗不總結就會忘記,而總結出來的內容卻不會被丟失。

如果對於重試你有更好的想法,歡迎提出交流探討,也歡迎關注我的公眾號進行留言交流。

相關推薦

最佳實踐如何優雅進行

本文口味:冰鎮楊梅 預計閱讀:20分鐘 說明 最近公司在搞活動,需要依賴一個第三方介面,測試階段並沒有什麼異常狀況,但上線後發現依賴的介面有時候會因為內部錯誤而返回系統異常,雖然概率不大,但總因為這個而報警總是不好的,何況死信佇列的訊息還需要麻煩運維進行重新投遞,所以加上重試機制勢在必行。 重試機制可以保護系

最佳實踐微信小程式客服訊息實時通知如何快速低成本實現?

我們做微信小程式開發的都知道,只要在小程式頁面中新增如下程式碼即可進入小程式的客服會話介面: <button open-type="contact" >聯絡我們</button> 微信小程式客服會話介面如下圖所示:

最佳實踐如何限制使用者僅通過HTTPS方式訪問OSS?

一、當前存在的問題   當前OSS支援使用者使用HTTPS/HTTP協議訪問Bucket。但由於HTTP存在安全漏洞。大型企業客戶都要求使用HTTPS方式訪問OSS,並且拒絕HTTP訪問請求。   目前OSS可以通過RAM policy方式實現:限制某個使用者、角色

OSS最佳實踐WEB站點中如何應用OSS產品

put discuz論壇 瓶頸 個人 得到 行為 ssim 雲安全 實現 【OSS最佳實踐】WEB站點中如何應用OSS產品http://www.bieryun.com/1194.htmlOSS提供了海量、安全、低成本、高可靠的雲存儲服務,用戶可以通過SDK、API、OSS相

taro最佳實踐設定好基礎開發字型尺寸

設定開發字型尺寸 我感覺在一個專案當中,務必一開始就設定好這個尺寸,關係後今後專案的一個統一管理問題。那麼應該怎麼設定這個呢? 設計思路 按照慣例,我們開發專案的時候,儘量不要為難自己,如果按照2倍圖,來開發的話,處處想著2倍,在開發的過程中,想參考其它專案的樣式

CDN 最佳實踐CDN快取策略解讀和配置策略

摘要: CDN 作為內容分發網路主要是將資源快取在 CDN 節點上,然後後續訪問即可直接通過 CDN 節點將資源返回給客戶端,而不再需要回到源站伺服器以加快請求速度。那麼 CDN 到底對於哪些請求加速呢?其快取規則和快取時間是怎麼樣的呢?怎麼樣的快取規則更加合理呢?本文就

Java實踐十二小球天平三次稱問題

十二個小球用天平三次稱重找出其中唯一一個質量不同(或輕或重)的小球,用java程式碼實現。 思路: 將十二個小球分別標記為A,B,C,D,E,F,G,H,I,J,K,L,將它們以四個為一組分為三組也就是:第一組:ABCD;第二組:EFGH;第三組:IJ

ECS最佳實踐基於多塊雲盤構建LVM邏輯卷

一、LVM簡介   LVM是邏輯盤卷管理(Logical Volume Manager)的簡稱,它是Linux環境下對磁碟分割

專案實踐後端介面統一規範的同時,如何優雅得擴充套件規範

> 以專案驅動學習,以實踐檢驗真知 # 前言 我在上一篇部落格中寫了如何通過引數校驗 + 統一響應碼 + 統一異常處理來構建一個優雅後端介面體系: [【專案實踐】SpringBoot三招組合拳,手把手教你打出優雅的後端介面](https://www.cnblogs.com/RudeCrab/p/1341

js 實踐js 實現木桶布局

cto enter 最後一行 scrip fine inner get code 兩個 還有兩個月左右就要準備實習了,所以特意練一練,今天終於搞定了js 的木桶布局了 這一個是按照一個插件的規格去寫的以防以後工作需要,詳細的解釋在前端網這裏 http://www.qdfun

工程實踐服務器數據解析

something 時間比較 數據訪問 shu 成員 字段值 ear 計時 日誌 本文來自網易雲社區作者:孫建順在客戶端開發過程中一個重點內容就是解析服務器數據,關於這個話題也許大家首先會去思考的問題是用哪個json解析庫。是的,目前通過json格式進行數據傳輸是主流的方式

ERC1155實踐歐陽詢書法復制品從確權設計到買賣測試

ply string doc gpu 構建 eas fontsize tab urn 作者介紹 筆名輝哥 副總(賦能中心)尖晶投資 1,摘要 【本文目標】通過本文學習,了解以太坊ERC1155標準規範和ERC1155Mintable可增發智能合約函數功能,並通過一個有趣的

HihoCoder - 1850字母去 (字串,思維)

題幹: 給定一個字串S,每次操作你可以將其中任意一個字元修改成其他任意字元。 請你計算最少需要多少次操作,才能使得S中不存在兩個相鄰的相同字元。 Input 只包含小寫字母的字串S。   1 ≤ |S| ≤ 100000 Output 一個整數代表答案

實驗五編寫、調具有多個段的程序

bubuko 中文 clas round lose dup 聲明 margin 提示 四、實驗結論 *任務(1)(2)(3)基本步驟相同,這裏只列舉出(1)的實驗步驟 步驟一:把要使用的代碼粘貼到masm文件夾中。 步驟二:編譯、連接、用debug調試。 步驟三:用r命令查

工程實踐伺服器資料解析

本文來自網易雲社群作者:孫建順在客戶端開發過程中一個重點內容就是解析伺服器資料,關於這個話題也許大家首先會去思考的問題是用哪個json解析庫。是的,目前通過json格式進行資料傳輸是主流的方式,確實不同的json解析庫在效能方面也有一些差異。以上問題固然重要,但是在開發過程中

第十七課 ERC721實踐迷戀貓從玩耍到開發

**CryptoKitties(中文名:迷戀貓)**是一款在以太坊區塊鏈上的虛擬養貓遊戲,一經推出就以病毒式的快速擴散,橫掃整個以太坊市場。而這款可愛的遊戲於2018年 2 月 16 日(農曆大年初一)登陸 iOS國區,中文名稱的 “迷戀貓”,皆因 “迷戀”

第二十課 ERC1155實踐歐陽詢書法複製品從確權設計到買賣測試

1,摘要 【本文目標】 通過本文學習,瞭解以太坊ERC1155標準規範和ERC1155Mintable可增發智慧合約函式功能,並通過一個有趣的故事完成功能測試。 【前置條件】 1)對以太坊ERC20(同質化代幣),ERC721(非同質化代幣)有所瞭解,對ERC

SpringCloud實踐之斷路器:Hystrix

一、服務雪崩效應 基礎服務的故障導致級聯故障,進而造成了整個分散式系統的不可用,這種現象被稱為服務雪崩效應。服務雪崩效應描述的是一種因服務提供者的不可用導致服務消費者的不可用,並將不可用逐漸放大的過程。 服務雪崩效應形成的原因 1、服務提供者不可用 硬體故障

資料庫-MySqlMysql 服務啟服務後5s左右自動關閉

背景 Window系統:Windows Server 2008 R2 資料庫版本:Mysql 5.5.8 AutoDM.err 181220 10:14:29 [Note] Plugin 'FEDERATED' is disabled. InnoDB: The InnoDB me

Scrum實踐——如何拆分Story

  去年入職公司不久,就趕上了公司“敏捷”開發的改革大潮。從最初的敏捷培訓,到摸著路探索,也有4個月的時間了。   現在,對於grooming,planning,daily stand up,demo review retrospective這些Scrum中的