1. 程式人生 > >使用Spring-retry 1.1.4完成重試功能

使用Spring-retry 1.1.4完成重試功能

前言

在實際專案中,經常需要在某種情況下對呼叫的方法進行重試,例如超時重試。通過Spring-retry能簡化重試功能的實現,並實現更多樣的重試操作。

Spring-retry結構

Spring-retry提供的RetryOperations介面,該介面提供了若干方法來執行重試操作。在Spring-retry 1.1.4 中該介面的定義如下。

public interface RetryOperations {
    <T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback) throws E;

    <T
, E extends Throwable> T execute(RetryCallback<T, E> retryCallback, RecoveryCallback<T> recoveryCallback) throws E; <T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback, RecoveryCallback<T> recoveryCallback, RetryState retryState) throws E; }

呼叫者通過傳入RetryCallback來完成呼叫者的重試操作。如果callback執行失敗(丟擲某些異常),那麼會按照呼叫者設定的策略進行重試。重試操作直到成功,或根據使用者設定的條件而退出。

RetryCallback的介面定義如下:

public interface RetryCallback<T, E extends Throwable> {
    T doWithRetry(RetryContext context) throws E;
}

Spring-retry提供了RetryOperations介面的實現類RetryTemplate。通過RetryTemplate來完成重試,下面是使用RetryTemplate重試的一個簡單例子。

public class Main {

    public static void main(String[] args) throws Exception {
        RetryTemplate template = new RetryTemplate();
        TimeoutRetryPolicy policy = new TimeoutRetryPolicy();

        template.setRetryPolicy(policy);

        String result = template.execute(
                new RetryCallback<String, Exception>() {
                    public String doWithRetry(RetryContext arg0) throws Exception {
                        return "Retry";
                    }
                }
        );
        System.out.println(result);
    }
}

程式碼定義了TimeoutRetryPolicy策略,TimeoutRetryPolicy超時時間預設是1秒。TimeoutRetryPolicy超時是指在execute方法內部,從open操作開始到呼叫TimeoutRetryPolicy的canRetry方法這之間所經過的時間。這段時間未超過TimeoutRetryPolicy定義的超時時間,那麼執行操作,否則丟擲異常。

protected <T, E extends Throwable> T doExecute(RetryCallback<T, E> retryCallback,RecoveryCallback<T> recoveryCallback, RetryState state) throws E,ExhaustedRetryException {
        ……略
        RetryContext context = open(retryPolicy, state);                 
        ……略
        while (canRetry(retryPolicy, context) &&   !context.isExhaustedOnly())   
        // 呼叫canRetry檢查是否可以重試
        ……略
}

RetryCallback中的方法doWithRetry有一個RetryContext型別的引數。大多數的doWithRetry操作都會會略該引數。但也可以在重試的過程中通過RetryContext來儲存資料,RetryContext結構如下。

這裡寫圖片描述

不同的RetryContext維護了不同型別的重試資訊,例如TimeoutRetryContext會維護超時時間與任務開始時間。

    private static class TimeoutRetryContext extends RetryContextSupport {
        private long timeout;

        private long start;

        public TimeoutRetryContext(RetryContext parent, long timeout) {
            super(parent);
            this.start = System.currentTimeMillis();
            this.timeout = timeout;
        }

        public boolean isAlive() {
            return (System.currentTimeMillis() - start) <= timeout;
        }
    }

執行重試時,具體的RetryContext型別,會根據使用者建立的重試策略來建立具體的RetryContext。例如,建立SimpleRetryPolicy,那麼最終建立的Context為SimpleRetryContext。

這裡寫圖片描述

org.springframework.core.AttributeAccessorSupport實現了org.springframework.core.AttributeAccessor,並定了一個Map,通過AttributeAccessor中定義的方法來訪問map,以便在重試過程中進行資料儲存與獲取資料。

在RetryOperations介面中還會看到RecoveryCallback這個引數。RecoveryCallback的定義如下。

public interface RecoveryCallback<T> {
    /**
     * @param context the current retry context
     * @return an Object that can be used to replace the   callback result that failed
     * @throws Exception when something goes wrong
     */
    T recover(RetryContext context) throws Exception;
}

當重試執行完閉,操作還未成為,那麼可以通過RecoveryCallback完成一些失敗事後處理。

public class Main {

    public static void main(String[] args) throws Exception {
        RetryTemplate template = new RetryTemplate();

        SimpleRetryPolicy policy = new SimpleRetryPolicy();
        policy.setMaxAttempts(2);

        template.setRetryPolicy(policy);

        String result = template.execute(
                new RetryCallback<String, Exception>() {
                    public String doWithRetry(RetryContext arg0) throws Exception {
                        throw new NullPointerException("nullPointerException");
                    }
                }
                ,
                new RecoveryCallback<String>() {
                    public String recover(RetryContext    context) throws Exception {
                        return "recovery callback";
                    }
                }
        );
        System.out.println(result);
    }
}

上面的程式碼重試兩次後,仍然失敗,RecoveryCallback被呼叫,返回”recovery callback”。如果沒有定義RecoveryCallback,那麼重試2次後,將會丟擲異常。

重試策略

重試策略定義了當操作失敗時如何進行重試操作,框架提供的重試策略包括。

SimpleRetryPolicy 策略

該策略定義了對指定的異常進行若干次重試。預設情況下,對Exception異常及其子類重試3次。如果建立SimpleRetryPolicy並指定重試異常map,可以選擇性重試或不進行重試。下面的程式碼定義了對TimeOutException進行重試

    public static void main(String[] args) throws Exception {
        RetryTemplate template = new RetryTemplate();
        Map<Class<? extends Throwable>, Boolean> maps = new HashMap<Class<? extends Throwable>, Boolean>();
        maps.put(TimeoutException.class, true);
        SimpleRetryPolicy policy2 = new SimpleRetryPolicy(2, maps);
        template.setRetryPolicy(policy2);


                new RetryCallback<String, Exception>() {
                    public String doWithRetry(RetryContext arg0) throws Exception {
                        throw new TimeoutException("TimeoutException");
                    }
                }
                ,
                new RecoveryCallback<String>() {
                    public String recover(RetryContext context) throws Exception {
                        return "recovery callback";
                    }
                }
        );
        System.out.println(result);
    }

當Map中的的value為false,那麼執行方法,隨後丟擲異常不進行重試。

NeverRetryPolicy 策略

執行一次待執行操作,若出現異常後不進行重試。

AlwaysRetryPolicy 策略

異常後一直重試直到成功。

TimeoutRetryPolicy 策略

在執行execute方法時從open操作開始到呼叫TimeoutRetryPolicy的canRetry方法這之間所經過的時間。這段時間未超過TimeoutRetryPolicy定義的超時時間,那麼執行操作,否則丟擲異常。

ExceptionClassifierRetryPolicy 策略

根據產生的異常選擇重試策略。

        Map<Class<? extends Throwable>, RetryPolicy> policyMap = new HashMap<Class<? extends Throwable>, RetryPolicy>();
        policyMap.put(TimeoutException.class, new AlwaysRetryPolicy());
        policyMap.put(NullPointerException.class, new NeverRetryPolicy());
        policy.setPolicyMap(policyMap);

通過PolicyMap定義異常及其重試策略。下面的程式碼在丟擲NullPointerException採用NeverRetryPolicy策略,而TimeoutException採用AlwaysRetryPolicy。

public class Main {

    public static void main(String[] args) throws Exception {
        RetryTemplate template = new RetryTemplate();
        ExceptionClassifierRetryPolicy policy = new ExceptionClassifierRetryPolicy();
        Map<Class<? extends Throwable>, RetryPolicy> policyMap = new HashMap<Class<? extends Throwable>, RetryPolicy>();

        policyMap.put(TimeoutException.class, new AlwaysRetryPolicy());
        policyMap.put(NullPointerException.class, new NeverRetryPolicy());
        policy.setPolicyMap(policyMap);

        template.setRetryPolicy(policy);
        String result = template.execute(
                new RetryCallback<String, Exception>() {
                    public String doWithRetry(RetryContext arg0) throws Exception {
                        if(arg0.getRetryCount() >= 2) {
                            Thread.sleep(1000);
                            throw new NullPointerException();   
                        }
                        throw new TimeoutException("TimeoutException");
                    }
                }
                ,
                new RecoveryCallback<String>() {
                    public String recover(RetryContext context) throws Exception {
                        return "recovery callback";
                    }
                }
        );
        System.out.println(result);
    }
}

此外可以通過setExceptionClassifier來為異常指定重試策略。

Classifier<Throwable, RetryPolicy> exceptionClassifier = new Classifier<Throwable, RetryPolicy>(){
            public RetryPolicy classify(Throwable classifiable) {
                if(classifiable instanceof TimeoutException)
                    return new SimpleRetryPolicy();
                return new NeverRetryPolicy();
            }

        };
        policy.setExceptionClassifier(exceptionClassifier);

setPolicyMap與setExceptionClassifier使用一個即可。

CompositeRetryPolicy 策略

使用者指定一組策略,隨後根據optimistic選項來確認如何重試。

下面的程式碼中建立CompositeRetryPolicy策略,並建立了RetryPolicy陣列,陣列有兩個具體策略SimpleRetryPolicy與AlwaysRetryPolicy。當CompositeRetryPolicy設定optimistic為true時,Spring-retry會順序遍歷RetryPolicy[]陣列,如果有一個重試策略可重試,例如SimpleRetryPolicy沒有達到重試次數,那麼就會進行重試。 如果optimistic選項設定為false。那麼有一個重試策略無法重試,那麼就不進行重試。例如SimpleRetryPolicy達到重試次數不能再重試,而AlwaysRetryPolicy可以重試,那麼最終是無法重試的。

public class Main {

    public static void main(String[] args) throws Exception {
        RetryTemplate template = new RetryTemplate();

        CompositeRetryPolicy policy = new CompositeRetryPolicy();
        RetryPolicy[] polices = {new SimpleRetryPolicy(), new AlwaysRetryPolicy()};

        policy.setPolicies(polices);
        policy.setOptimistic(true);
        template.setRetryPolicy(policy);

        String result = template.execute(
                new RetryCallback<String, Exception>() {
                    public String doWithRetry(RetryContext arg0) throws Exception {
                        if(arg0.getRetryCount() >= 2) {
                            Thread.sleep(1000);
                            throw new NullPointerException();

                        }
                        throw new TimeoutException("TimeoutException");
                    }
                }
                ,
                new RecoveryCallback<String>() {
                    public String recover(RetryContext context) throws Exception {
                        return "recovery callback";
                    }
                }
        );
        System.out.println(result);
    }
}

上述程式碼,設定setOptimistic(true),而AlwaysRetryPolicy一直可重試,那麼最終可以不斷進行重試。

退避(BackOff)策略

當操作執行失敗時,根據設定的重試策略進行重試。通過BackoffPolicy可以設定再次重試的時間間隔。

public interface BackOffPolicy {
    BackOffContext start(RetryContext context);
    void backOff(BackOffContext backOffContext) throws BackOffInterruptedException;
}

BackOffPolicy介面的實現具體的實現有如下幾種。
這裡寫圖片描述

StatelessBackoffPolicy下的3個實現類

org.springframework.retry.backoff.FixedBackOffPolicy
在等待一段固定的時間後,再進行重試。預設為1秒。

org.springframework.retry.backoff.NoBackOffPolicy
實現了空方法,因此採用次策略,重試不會等待。這也是RetryTemplate採用的預設退避(backOff)策略。

private volatile BackOffPolicy backOffPolicy = new NoBackOffPolicy();

org.springframework.retry.backoff.UniformRandomBackOffPolicy
均勻隨機退避策略,等待時間為 最小退避時間 + [0,最大退避時間 - 最小退避時間)間的一個隨機數,如果最大退避時間等於最小退避時間那麼等待時間為0。

SleepingbackOffPolicy的下的4個實現類

org.springframework.retry.backoff.ExponentialBackOffPolicy
指數退避策略 ,每次等待時間為 等待時間 = 等待時間 * N ,即每次等待時間為上一次的N倍。如果等待時間超過最大等待時間,那麼以後的等待時間為最大等待時間。

        ExponentialBackOffPolicy exponentialBackOffPolicy = new ExponentialBackOffPolicy();
        exponentialBackOffPolicy.setInitialInterval(1500);
        exponentialBackOffPolicy.setMultiplier(2);
        exponentialBackOffPolicy.setMaxInterval(6000);

上面的程式碼建立了一個ExponentialBackOffPolicy,初始時間間隔為1500毫秒,N = 2,¸最大間隔為6000毫秒,那麼從第4次重試開始,以後每次等待時間都為6000毫秒。

org.springframework.retry.backoff.ExponentialRandomBackOffPolicy
指數隨機策略,該類是ExponentialBackOffPolicy的子類,計算等待時間的演算法為

        @Override
        public synchronized long getSleepAndIncrement() {
            long next = super.getSleepAndIncrement();
            next = (long)(next*(1 + r.nextFloat()*(getMultiplier()-1)));
            return next;
        }

該方法呼叫ExponentialBackOffPolicy方法,返回等待時間,隨後next*(Multiplier – 1)最為等待時間。 即 [next,next* Multiplier) 之間的一個隨機數。

org.springframework.retry.backoff.FixedBackOffPolicy
org.springframework.retry.backoff.UniformRandomBackOffPolicy
這兩個類與StatelessBackoffPolicy的同名實現類返回等待時間的方法是一致的。而兩者的主要區別是,SleepingbackOffPolicy可以設定使用者定義的Sleeper。

public interface Sleeper {
    void sleep(long backOffPeriod) throws InterruptedException;
}

監聽器

通過監聽器,可以在重試操作的某些位置嵌入呼叫者定義的一些操作,以便在某些場景觸發。

public class Main {

    public static void main(String[] args) throws Exception {
        RetryTemplate template = new RetryTemplate();

        ExponentialRandomBackOffPolicy exponentialBackOffPolicy = new ExponentialRandomBackOffPolicy();
        exponentialBackOffPolicy.setInitialInterval(1500);
        exponentialBackOffPolicy.setMultiplier(2);
        exponentialBackOffPolicy.setMaxInterval(6000);


        CompositeRetryPolicy policy = new CompositeRetryPolicy();
        RetryPolicy[] polices = {new SimpleRetryPolicy(), new AlwaysRetryPolicy()};

        policy.setPolicies(polices);
        policy.setOptimistic(true);

        template.setRetryPolicy(policy);
        template.setBackOffPolicy(exponentialBackOffPolicy);

        template.registerListener(new RetryListener() {

            public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
                System.out.println("open");
                return true;
            }

            public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback,
                    Throwable throwable) {

                System.out.println("onError");
            }

            public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback,
                    Throwable throwable) {
                System.out.println("close");
            }
        });

        template.registerListener(new RetryListener() {

            public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
                System.out.println("open2");
                return true;
            }

            public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback,
                    Throwable throwable) {

                System.out.println("onError2");
            }

            public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback,
                    Throwable throwable) {
                System.out.println("close2");
            }
        });
        String result = template.execute(
                new RetryCallback<String, Exception>() {
                    public String doWithRetry(RetryContext arg0) throws Exception {
                        arg0.getAttribute("");
                        if(arg0.getRetryCount() >= 2) {
                            throw new NullPointerException();
                        }
                        throw new TimeoutException("TimeoutException");
                    }
                }
        );
        System.out.println(result);
    }
}

上述程式碼註冊了兩個Listener,Listener中的三個實現方法,onError,open,close會在執行重試操作時被呼叫,在RetryTemplate中doOpenInterceptors,doCloseInterceptors,doOnErrorInterceptors會呼叫監聽器對應的open,close,onError 方法。

doOpenInterceptors方法在第一次重試之前會被呼叫,如果該方法返回true,則會繼續向下直接,如果返回false,則丟擲異常,停止重試。

doCloseInterceptors 會在重試操作執行完畢後呼叫。

doOnErrorInterceptors 在丟擲異常後執行,

當註冊多個Listener時,open方法按會按Listener的註冊順序呼叫,而onError和close則按Listener註冊的順序逆序呼叫。

參考