Spring Retry框架——看這篇就夠了

簡介

    軟件架構從當初的單機,演變到後來的集羣,再到後來的分佈式應用。原本看似可以信任的服務調用,加上了網絡因素就變得不再可靠。再考慮到一些調用鏈路的特殊性,又要保證性能,又要儘可能增加成功率,所以調用方必須肩負起重試的責任。

 

自己寫,怎樣實現?

    重試並不複雜,首先來分析下重試的調用場景,可以想到業務當中不止一處會需要重試能力,並且業務其實更關乎自己的代碼塊被重試就可以了,而不在乎如何實現的重試。

    所以變化的是一段可以重複執行的代碼塊,以及重試次數等。

① 代碼塊可以用Java8支持的函數式編程解決

② 次數可以用入參/配置實現

    首先定義一個執行的模版,結合上面我們的分析,這個模版需要一個待實行的方法塊,以及配置。(次數等區分場景,用枚舉定義,方便全局管控):

@AllArgsConstructor
@Getter
public enum RetrySceneEnums {

    QUERY_USER_INFO("查詢用戶信息", 2),
    ;

    private String desc;

    private int retryTimes;
}
abstract class MyRetryTemplate<Req, Resp> {
    /** 配置 */
    private IntegrationRetryEnums retryEnum;
    /** 方法塊 */
    private Function<Req, Resp> fuction;

    MyRetryTemplate(IntegrationRetryEnums retryEnum, Function<Req, Resp> fuction) {
        this.retryEnum = retryEnum;
        this.fuction = fuction;
    }

    /**
     * 構建函數
     */
    private Supplier<Function<Req, Resp>> retry = () -> (param) -> {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        return execute(param, 1, stopWatch);
    };

    private Resp execute(Req param, int index, StopWatch stopWatch) {
        if (index > retryEnum.getRetryTimes()) {
            stopWatch.split();
            // 最大的重試次數後仍失敗,可以打印摘要日誌
            return null;
        }
        try {
            // 執行方法塊
            Resp result = fuction.apply(param);
            stopWatch.split();
            // 認爲成功
            return result;
        } catch (Throwable e) {
            // 遞歸重試
            return execute(param, ++index, stopWatch);
        }
    }

    Function<Req, Resp> getRetryService() {
        return retry.get();
    }
}

    邏輯很簡單,利用遞歸思想(當然也可以用循環),默認調用方法拋出異常纔會重試,成功則返回,否則遞歸直到最大次數,使用 StopWatch 統計總耗時,業務可以打印摘要日誌並配置監控等,代碼簡短,但基本實現了功能。

    對拋出異常纔會重試稍微解釋下,爲什麼不支持對業務成功/失敗進行重試。首先重試框架感知不到具體的業務響應結構,進而也無法判斷業務的成功與失敗,當然重試框架也可以自定義一個響應體並強制使用,但這便於業務代碼形成耦合。再退一步講,也沒有必要對業務失敗進行重試,因爲框架畢竟是框架,只需要做通用的事情,業務的失敗到底需不需要重試,需要看具體的場景。

    模板完工了,看下具體怎樣改造一個已有的方法,使其支持重試功能,假設當前有一個查詢用戶信息的方法 UserService#queryById(String id) 。需要在 Spring 啓動後將其包裝以支持重試,通過事件機制監聽 ContextRefreshedEvent 事件。

@Component
public class RetryServiceFactory implements ApplicationListener<ContextRefreshedEvent> {

    private static AtomicBoolean INIT = new AtomicBoolean(false);

    @Autowired
    private UserService userService;

    private Function<String, WorkOrderDTO> uerQueryFunction;

    @Override
    public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
        if (INIT.compareAndSet(false, true)) {
            this.uerQueryFunction = new MyRetryTemplate<String, WorkOrderDTO>(QUERY_USER_INFO, userService::queryById) {
            }.getRetryService();
        }
    }

    public Function<String, WorkOrderDTO> getUserQueryService() {
        return uerQueryFunction;
    }
}

    需要使用此能力的地方,這麼調用即可:

@Autowired
private RetryServiceFactory retryServiceFactory;

public void businessLogic(String userId){

    // 調用工廠獲取自帶重試功能的服務,並執行
    UserInfo userInfo = retryServiceFactory.getUserQueryService().apply(userId);
}

    到此,自己手寫的重試工具已經完成,如果執行時遇到網絡抖動,超時等,都會進行最大2次的重試(次數由 RetrySceneEnums 設置)。

 

    這時候再回頭審視下代碼,可以發現諸多侷限性:

  • 比如 Function 的使用,就限定了入參只能是一個,且必須有返回(即不支持 void);
  • 每次接入一個服務,就要在 RetryServiceFactory 擴充;
  • 考慮網絡抖動可能有區間性,盲目連續請求可能全部失敗,如要實現等待一段實現再重試,怎麼辦?
  • 假使底層服務真的掛了,但上層業務系統依舊重試,重試每次都以超時告終(佔用線程資源),流量上來以後,必然會牽連業務系統也不可用。自然想到熔斷,那重試如何加入斷路器(Circuit Breaker)模式?

    遇到共性問題,首先要想到有沒有現成的框架可以使用,而不是造輪子。目前我已知的重試框架,有 spring-retry 以及 guava-retrying,接下來將對 spring-retry 的使用和原理進行講解。

 

Spring Retry 使用

    同spring事務框架一樣,retry框架同樣支持 編程式聲明式 兩種。僅需要一個maven依賴。如果使用聲明式,需要額外引入 aspectj 。

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

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.2</version>
    <scope>runtime</scope>
</dependency>

編程式

    先來介紹下編程式使用到的幾個主要組件:

  • 重試模板類:org.springframework.retry.support.RetryTemplate
  • 重試策略類:org.springframework.retry.RetryPolicy
  • 重試回退策略(兩次重試間等待策略):org.springframework.retry.backoff.BackOffPolicy
  • 重試上下文:org.springframework.retry.RetryContext
  • 監聽器:org.springframework.retry.RetryListener

   接下來來看下如何使用,然後穿插講下不同策略的區別。

    首先是開啓重試,並聲明一個模板bean。因爲只是模板,所以可以藉助 Spring IOC 聲明爲單例。

@Configuration
@EnableRetry(proxyTargetClass = true)
public class RetryConfig {

    @Bean
    public RetryTemplate simpleRetryTemplate() {
        RetryTemplate retryTemplate = new RetryTemplate();
        // 最大重試次數策略
        SimpleRetryPolicy simpleRetryPolicy = new SimpleRetryPolicy(3);
        retryTemplate.setRetryPolicy(simpleRetryPolicy);
        FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
        // 每隔1s後再重試
        fixedBackOffPolicy.setBackOffPeriod(1000);
        retryTemplate.setBackOffPolicy(fixedBackOffPolicy);
        return retryTemplate;
    }

}

    例子代碼使用了 SimpleRetryPolicy 和 FixedBackOffPolicy,當然還有其他重試策略和回退策略。枚舉如下:

重試策略

策略類 效果 關鍵參數
MaxAttemptsRetryPolicy 設置最大的重試次數,超過之後執行recover maxAttempts:最大重試次數
BinaryExceptionClassifierRetryPolicy 可以指定哪些異常需要重試,哪些異常不需要重試 exceptionClassifier:BinaryExceptionClassifier,異常識別類,其實就是存在一個Map映射,Map<Class<? extends Throwable>, Boolean>,value爲true的話就說明需要重試。
SimpleRetryPolicy 上面兩者的結合,支持次數和自定義異常重試 maxAttempts 和 exceptionClassifier
TimeoutRetryPolicy 在指定的一段時間內重試 timeout:單位ms
ExceptionClassifierRetryPolicy

支持異常和重試策略的映射,比如異常A對應重試策略A,異常B對應重試策略B

exceptionClassifier:本質就是異常和重試策略的映射
CompositeRetryPolicy 策略類的組合類,支持兩種模式,樂觀模式:只要一個重試策略滿足即執行,悲觀模式:另一種是所有策略模式滿足再執行 policies:策略數組
optimistic:true-樂觀;false-悲觀
CircuitBreakerRetryPolicy 熔斷器模式 delegate:策略代理類
openTimeout:[0,openTimeout]會一直執行代理類策略,(openTimeout, resetTimeout] 會半打開,如果代理類判斷不可以重試,就會熔斷,執行recover邏輯,如果代理類判斷還可以重試且重試成功,開關會閉合。
resetTimeout:超過指定時間,開關重置閉合
ExpressionRetryPolicy 符合表達式就重試 expression:表達式,見 org.springframework.expression.Expression實現
AlwaysRetryPolicy 一直重試  
NeverRetryPolicy 從不重試  

回退策略

策略類 效果 關鍵參數
FixedBackOffPolicy 間隔固定時間重試 sleeper:支持線程sleep和Object#wait,區別在於是否釋放鎖
backOffPeriod:等待間隔
NoBackOffPolicy 無等待直接重試  
UniformRandomBackOffPolicy 在一個設置的時間區間內。隨機等待後重試 minBackOffPeriod:區間下限
maxBackOffPeriod:區間上限
sleeper:同上
ExponentialBackOffPolicy 在一個設置的時間區間內,等待時長爲上一次時長的遞增 initialInterval:默認起始等待時間
maxInterval:最大等待時間
multiplier:遞增倍數
sleeper:同上
ExponentialRandomBackOffPolicy 在 ExponentialBackOffPolicy 基礎上,乘數隨機  

    至此,完成了重試模板的定義。接下來是需要在具體的業務場景下調用模板方法即可。同樣的,爲了保證複用性,仍然藉助函數式編程定義:

@Service
public class RetryTemplateService {

    @Autowired
    private RetryTemplate simpleRetryTemplate;

    public <R, T> R retry(Function<T, R> method, T param) throws Throwable {
        return simpleRetryTemplate.execute(new RetryCallback<R, Throwable>() {
            @Override
            public R doWithRetry(RetryContext context) throws Throwable {
                return method.apply(param);
            }
        }, new RecoveryCallback<R>() {
            @Override
            public R recover(RetryContext context) throws Exception {
                return null;
            }
        });
    }
}
@Service
public class UserService {

    @Autowired
    private RetryTemplateService retryTemplateService;

    public User queryById(String id) {
        // TODO 業務邏輯
    }

    public User queryByIdWithRetry(String id) throws Throwable {
        return retryTemplateService.retry(this::queryById, id);
    }
}

    通過調用 org.springframework.retry.support.RetryTemplate#execute,指定需要支持重試的業務代碼回調 org.springframework.retry.RetryCallback,以及全部重試失敗的兜底回調 org.springframework.retry.RecoveryCallback

    RetryTemplate 支持四種重載的 execute 方法,這裏不全部展開分析,大體十分相似,變化的是策略(見上面表格),以及 RetryState(有無狀態,下面分析)。

聲明式

    通過上面的編碼式,還需要針對不同場景編寫一到多套模板方法,那有沒有什麼可以簡化這一步呢,畢竟業務沒必要關注這些具體的模板代碼。就像事務一樣,只需要方法上加 @Transactional 就可以了。當然,spring-retry 也支持,直接上代碼:

@Service
public class UserService {

    @Retryable(include = RetryException.class, exclude = IllegalArgumentException.class, 
      listeners = {"defaultRetryListener"}, backoff = @Backoff(delay = 1000, maxDelay = 2000))
    public User queryById(String id) {
        // TODO 業務邏輯
    }

    @Recover
    public User recover(RetryException e, String id) {
        User user = new User();
        user.setName("兜底");
        return user;
    }
}
@Service
public class DefaultRetryListener implements RetryListener {

    @Override
    public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
        System.out.println("==前置回調==");
        return true;
    }

    @Override
    public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
        System.out.println("==後置回調==");
    }

    @Override
    public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
        System.out.println("==執行報錯==");
    }
}

    這樣,直接調用 UserService#queryById 就支持重試了,比編程式節省了不少工作量。

    我這裏一股腦設置了很多註解參數,其實都對應上面編程式的策略,畢竟編程式有的,我聲明式也得要。使用時按需即可,接下來對照着 @Retryable 中定義的所有屬性,解釋下含義:

  • recover:指定兜底/補償的方法名。如果不指定,默認對照 @Recover 標識的,第一入參爲重試異常,其餘入參和出參一致的方法;
  • interceptor:指定方法切面 bean,org.aopalliance.intercept.MethodInterceptor 實現類
  • value / include:兩者用途一致,指出哪些類型需要重試;
  • exclude:和 include 相反,指出哪些異常不需要重試;
  • label:可以指定唯一標籤,用於統計;
  • stateful:默認false。重試是否是有狀態的;
  • maxAttempts:最大的重試次數;
  • backoff:指定 @Backoff,回退策略;
  • listeners:指定 org.springframework.retry.RetryListener 實現 bean。

    對照着上面編程式,可以看出回退策略用註解 @Backoff 支持了。這裏還補充了上面編程式沒有用到的 RetryListener,它定義了3個方法:

<T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback);
<T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable);
<T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable);

    open 和 close 分別在重試整體的前置和後置被回調一次,onError 則是重試整體過程中,每次異常都會被回調。(具體細節見下面 源碼分析

    @Backoff 的屬性如下,基本都對應上面編程式的幾種策略,僅簡單解釋下

  • value / delay:兩者都標識延遲時間,爲 0則對應 NoBackOffPolicy 策略。
  • maxDelay:最大延遲時間
  • multiplier:遞增乘數
  • random:遞增乘數是否隨機

    重試模式中的斷路器,由於其場景的特殊性以及屬性的複雜性,則被單獨定義成了一個註解 @CircuitBreaker,從註釋的定義可以看出,它是在 @Retryable 基礎之上的擴展。

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Retryable(stateful = true)
public @interface CircuitBreaker {
   ....
}

    其中屬性的定義,也是在 @Retryable 的屬性基礎上,額外擴充了斷路器特有的幾個屬性,對應上面重試策略表格中——CircuitBreakerRetryPolicy 的關鍵參數:

  • resetTimeout:重置閉合超時時間
  • openTImeout:半打開狀態超時時間

 

源碼解析

    那具體 spring-retry 的原理是怎樣的呢?是遞歸還是循環,註解又是怎樣實現的?帶着這些問題開始源碼分析,順帶着可以看下上面沒有講到的 org.springframework.retry.RetryContext 組件在業務中如何使用。 

    首先從模板方法源碼開始:

public class RetryTemplate implements RetryOperations {

	@Override
	public final <T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback,
			RecoveryCallback<T> recoveryCallback) throws E {
		return doExecute(retryCallback, recoveryCallback, null);
	}

    protected <T, E extends Throwable> T doExecute(RetryCallback<T, E> retryCallback,
                                                   RecoveryCallback<T> recoveryCallback, RetryState state) throws E, ExhaustedRetryException {

        // 通過 set方法設置的重試策略,默認 SimpleRetryPolicy
        RetryPolicy retryPolicy = this.retryPolicy;
        // 通過 set方法設置的回退策略,默認 NoBackOffPolicy
        BackOffPolicy backOffPolicy = this.backOffPolicy;

        // 無狀態的:重試策略自定義 org.springframework.retry.RetryPolicy.open
        // 有狀態的:根據策略,每次新建/從緩存獲取
        RetryContext context = open(retryPolicy, state);
        if (this.logger.isTraceEnabled()) {
            this.logger.trace("RetryContext retrieved: " + context);
        }
        // 綁定到 ThreadLocal 以便線程內全局獲取
        RetrySynchronizationManager.register(context);

        Throwable lastException = null;

        boolean exhausted = false;
        try {
            // 回調所有的 org.springframework.retry.RetryListener.open
            boolean running = doOpenInterceptors(retryCallback, context);
            // 任意一個監聽器的open返回 false則拋出異常
            if (!running) {
                throw new TerminatedRetryException("Retry terminated abnormally by interceptor before first attempt");
            }

            // Get or Start the backoff context...
            BackOffContext backOffContext = null;
            Object resource = context.getAttribute("backOffContext");

            if (resource instanceof BackOffContext) {
                backOffContext = (BackOffContext) resource;
            }

            if (backOffContext == null) {
                // 回調重試策略的 start 方法
                backOffContext = backOffPolicy.start(context);
                if (backOffContext != null) {
                    context.setAttribute("backOffContext", backOffContext);
                }
            }

            // 回調 org.springframework.retry.RetryPolicy.canRetry 判斷是否可以重試
            // 同時可以調用 org.springframework.retry.RetryContext.setExhaustedOnly進行干預,不再進行重試
            while (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) {

                try {
                    if (this.logger.isDebugEnabled()) {
                        this.logger.debug("Retry: count=" + context.getRetryCount());
                    }
                    lastException = null;
                    // 回調業務代碼塊
                    return retryCallback.doWithRetry(context);
                } catch (Throwable e) {
                    lastException = e;
                    try {
                        // 回調 org.springframework.retry.RetryPolicy.registerThrowable
                        registerThrowable(retryPolicy, state, context, e);
                    } catch (Exception ex) {
                        throw new TerminatedRetryException("Could not register throwable", ex);
                    } finally {
                        // 回調 org.springframework.retry.RetryListener.onError
                        doOnErrorInterceptors(retryCallback, context, e);
                    }
                    // 同上面的判斷一樣,允許重試的話,會回調回退策略
                    if (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) {
                        try {
                            // 這一步會根據策略進行等待/立即執行
                            backOffPolicy.backOff(backOffContext);
                        } catch (BackOffInterruptedException ex) {
                            lastException = e;
                            if (this.logger.isDebugEnabled()) {
                                this.logger.debug("Abort retry because interrupted: count=" + context.getRetryCount());
                            }
                            throw ex;
                        }
                    }

                    if (this.logger.isDebugEnabled()) {
                        this.logger.debug("Checking for rethrow: count=" + context.getRetryCount());
                    }
                    // 如果指定了 org.springframework.retry.RetryState,會判斷是否針對該異常進行拋出,即進行重試阻斷
                    if (shouldRethrow(retryPolicy, context, state)) {
                        if (this.logger.isDebugEnabled()) {
                            this.logger.debug("Rethrow in retry for policy: count=" + context.getRetryCount());
                        }
                        throw RetryTemplate.<E>wrapIfNecessary(e);
                    }

                }

                if (state != null && context.hasAttribute(GLOBAL_STATE)) {
                    break;
                }
            }

            if (state == null && this.logger.isDebugEnabled()) {
                this.logger.debug("Retry failed last attempt: count=" + context.getRetryCount());
            }

            exhausted = true;
            // 回調兜底/補償 org.springframework.retry.RecoveryCallback.recover
            return handleRetryExhausted(recoveryCallback, context, state);

        } catch (Throwable e) {
            throw org.springframework.retry.support.RetryTemplate.<E>wrapIfNecessary(e);
        } finally {
            // 回調 org.springframework.retry.RetryPolicy.close
            close(retryPolicy, context, state, lastException == null || exhausted);
            // 回調 org.springframework.retry.RetryListener.close
            doCloseInterceptors(retryCallback, context, lastException);
            // 清除 ThreadLoacal中存儲的 org.springframework.retry.RetryContext
            RetrySynchronizationManager.clear();
        }

    }
}

    整體模板方法不是很複雜,上面註釋也標明具體回調哪些方法。大體邏輯就是:

初始化上下文並綁定 -> 回調監聽器open判斷是否可以重試 -> 回調重試策略start方法 -> 通過重試策略判斷是否可以重試 -> 可以的話執行業務代碼塊 -> 下面兩個分支 成功/異常
成功:回調重試策略的close -> 回調監聽器的close -> 清除上下文              鏈路 
異常:回調重試策略的registerThrowable -> 回調監聽器的onError -> 回調回退策略的backoff -> 如果設置了 Retrystate,判斷是否需要拋異常阻斷重試 -> 兜底/補償邏輯 -> 鏈路 

    大體流程瞭解後,先來看下上下文接口的定義:

public interface RetryContext extends AttributeAccessor {

    String NAME = "context.name";  // 上下文的自定義名稱

    String STATE_KEY = "context.state";  // RetryState定義的key

    String CLOSED = "context.closed"; // 標識重試是否close

    String RECOVERED = "context.recovered";  // 標識是否執行了兜底/補償

    String EXHAUSTED = "context.exhausted";  // 標識重試最大次數仍失敗

	void setExhaustedOnly();

	boolean isExhaustedOnly();

	RetryContext getParent();

	int getRetryCount();

	Throwable getLastThrowable();
}
public interface AttributeAccessor {
    void setAttribute(String name, @Nullable Object value);

    @Nullable
    Object getAttribute(String name);

    @Nullable
    Object removeAttribute(String name);

    boolean hasAttribute(String name);

    String[] attributeNames();
}

    加上繼承類 AttributeAccessor 支持的key-value存儲,作爲上下文,貫穿重試框架的一次調用。框架定義的key常量接口內定義了部分,還有一些分散在實現類以及策略類中。如果業務所需,也可以放置數據到上下文,在一次重試策略調用週期內共享。

    那就來看下上下文的初始化邏輯:

public class RetryTemplate implements RetryOperations {

    protected RetryContext open(RetryPolicy retryPolicy, RetryState state) {
        // 如果是無狀態的,每次都調用 org.springframework.retry.RetryPolicy.open創建
        if (state == null) {
            return doOpenInternal(retryPolicy);
        }

        Object key = state.getKey();
        // 如果有狀態,但設置了強制刷新,同樣 org.springframework.retry.RetryPolicy.open創建,並寫入 state.key
        if (state.isForceRefresh()) {
            return doOpenInternal(retryPolicy, state);
        }
        // 嘗試從緩存獲取,不存在走新建
        if (!this.retryContextCache.containsKey(key)) {
            // The cache is only used if there is a failure.
            return doOpenInternal(retryPolicy, state);
        }
        // 緩存獲取
        RetryContext context = this.retryContextCache.get(key);
        if (context == null) {
            // 異常場景
            if (this.retryContextCache.containsKey(key)) {
                throw new RetryException("Inconsistent state for failed item: no history found. "
                        + "Consider whether equals() or hashCode() for the item might be inconsistent, "
                        + "or if you need to supply a better ItemKeyGenerator");
            }
            // 因爲整體沒有鎖,所以有這一步作爲補償(沒仔細琢磨,會不會有併發邏輯)
            return doOpenInternal(retryPolicy, state);
        }

        // 因爲是緩存,所以同一個 state.key 會共享這個上下文,清除會影響其他正在校驗這幾個key的地方
        context.removeAttribute(RetryContext.CLOSED);
        context.removeAttribute(RetryContext.EXHAUSTED);
        context.removeAttribute(RetryContext.RECOVERED);
        return context;

    }
}

    從這裏可以看出,如果個別場景想要在多次調用間共享 RetryContext,就需要定義 state.key,且設置 forceRefresh=false。每次重試獨佔上下文的話,要麼就使用無狀態的重試,要麼就設置 forceRefresh=true。

    到此爲止,基本線索都集中到 RetryPolicy 的實現上了,基本都是回調 RetryPolicy 定義的方法。話不多說,來看兩個重試策略的實現。

public class SimpleRetryPolicy implements RetryPolicy {

    private volatile int maxAttempts;

    private BinaryExceptionClassifier retryableClassifier = new BinaryExceptionClassifier(false);

    public SimpleRetryPolicy(int maxAttempts, BinaryExceptionClassifier classifier) {
        super();
        this.maxAttempts = maxAttempts;
        this.retryableClassifier = classifier;
    }

    @Override
    public boolean canRetry(RetryContext context) {
        // 獲取最新一次重試的異常
        Throwable t = context.getLastThrowable();
        // 允許重試的異常 並且 次數<最大重試次數
        return (t == null || retryForException(t)) && context.getRetryCount() < this.maxAttempts;
    }

    @Override
    public void close(RetryContext status) {
    }

    @Override
    public void registerThrowable(RetryContext context, Throwable throwable) {
        SimpleRetryContext simpleContext = ((SimpleRetryContext) context);
        // 記錄最近一次異常,並自增重試次數
        simpleContext.registerThrowable(throwable);
    }

    @Override
    public RetryContext open(RetryContext parent) {
        return new SimpleRetryContext(parent);
    }

    private static class SimpleRetryContext extends RetryContextSupport {

        public SimpleRetryContext(RetryContext parent) {
            super(parent);
        }

    }

    private boolean retryForException(Throwable ex) {
        return this.retryableClassifier.classify(ex);
    }

}
public class CircuitBreakerRetryPolicy implements RetryPolicy {

    public static final String CIRCUIT_OPEN = "circuit.open";

    public static final String CIRCUIT_SHORT_COUNT = "circuit.shortCount";

    private static Log logger = LogFactory.getLog(CircuitBreakerRetryPolicy.class);

    private final RetryPolicy delegate;

    private long resetTimeout = 20000;

    private long openTimeout = 5000;

    public CircuitBreakerRetryPolicy(RetryPolicy delegate) {
        this.delegate = delegate;
    }

    public void setResetTimeout(long timeout) {
        this.resetTimeout = timeout;
    }

    public void setOpenTimeout(long timeout) {
        this.openTimeout = timeout;
    }

    @Override
    public boolean canRetry(RetryContext context) {
        CircuitBreakerRetryContext circuit = (CircuitBreakerRetryContext) context;
        // 判斷斷路器開關是否打開
        if (circuit.isOpen()) {
            // 打開則不允許重試
            circuit.incrementShortCircuitCount();
            return false;
        } else {
            circuit.reset();
        }
        // 斷路器開關閉合 or 半打開,是否允許重試交給代理實現
        return this.delegate.canRetry(circuit.context);
    }

    @Override
    public RetryContext open(RetryContext parent) {
        return new CircuitBreakerRetryContext(parent, this.delegate, this.resetTimeout, this.openTimeout);
    }

    @Override
    public void close(RetryContext context) {
        CircuitBreakerRetryContext circuit = (CircuitBreakerRetryContext) context;
        // 代理
        this.delegate.close(circuit.context);
    }

    @Override
    public void registerThrowable(RetryContext context, Throwable throwable) {
        CircuitBreakerRetryContext circuit = (CircuitBreakerRetryContext) context;
        circuit.registerThrowable(throwable);
        // 代理
        this.delegate.registerThrowable(circuit.context, throwable);
    }

    static class CircuitBreakerRetryContext extends RetryContextSupport {

        private volatile RetryContext context;

        private final RetryPolicy policy;

        private volatile long start = System.currentTimeMillis();

        private final long timeout;

        private final long openWindow;

        private final AtomicInteger shortCircuitCount = new AtomicInteger();

        public CircuitBreakerRetryContext(RetryContext parent, RetryPolicy policy, long timeout, long openWindow) {
            super(parent);
            this.policy = policy;
            this.timeout = timeout;
            this.openWindow = openWindow;
            this.context = createDelegateContext(policy, parent);
            setAttribute("state.global", true);
        }

        public void reset() {
            shortCircuitCount.set(0);
            setAttribute(CIRCUIT_SHORT_COUNT, shortCircuitCount.get());
        }

        public void incrementShortCircuitCount() {
            shortCircuitCount.incrementAndGet();
            setAttribute(CIRCUIT_SHORT_COUNT, shortCircuitCount.get());
        }

        private RetryContext createDelegateContext(RetryPolicy policy, RetryContext parent) {
            RetryContext context = policy.open(parent);
            reset();
            return context;
        }

        /* 判斷斷路器開關是否打開  **/
        public boolean isOpen() {
            // 計算時間間隔
            long time = System.currentTimeMillis() - this.start;
            // 判斷是否允許重試
            boolean retryable = this.policy.canRetry(this.context);
            if (!retryable) {
                // 閉合重置開關超時時間
                if (time > this.timeout) {
                    logger.trace("Closing");
                    // 重建上下文
                    this.context = createDelegateContext(policy, getParent());
                    this.start = System.currentTimeMillis();
                    // 判斷是否允許重試
                    retryable = this.policy.canRetry(this.context);
                }
                // [0,openTimeout)
                else if (time < this.openWindow) {
                    // 指定開關打開
                    if ((Boolean) getAttribute(CIRCUIT_OPEN) == false) {
                        logger.trace("Opening circuit");
                        setAttribute(CIRCUIT_OPEN, true);
                    }
                    this.start = System.currentTimeMillis();
                    return true;
                }
            }
            // 允許重試
            else {
                // (openWindow,resetTimeout]
                if (time > this.openWindow) {
                    logger.trace("Resetting context");
                    // 重建上下文
                    this.start = System.currentTimeMillis();
                    this.context = createDelegateContext(policy, getParent());
                }
            }
            if (logger.isTraceEnabled()) {
                logger.trace("Open: " + !retryable);
            }
            // 設置開關打開的標誌位,不能重試=開關打開
            setAttribute(CIRCUIT_OPEN, !retryable);
            return !retryable;
        }

        @Override
        public int getRetryCount() {
            return this.context.getRetryCount();
        }

        @Override
        public String toString() {
            return this.context.toString();
        }

    }

}

    除了斷路器策略(CircuitBreakerRetryPolicy)稍微複雜點,其他基本都像 SimpleRetryPolicy 一樣,邏輯簡單。

    首先 canRetry 在模板方法內是判斷是否要重試的實現。SimpleRetryPolicy 實現就是判斷了次數和異常,是否滿足要求。CircuitBreakerRetryPolicy,主要邏輯集中在 org.springframework.retry.policy.CircuitBreakerRetryPolicy.CircuitBreakerRetryContext#isOpen,開關打開不允許重試,否則根據代理類判斷是否允許重試。

   

    重試策略看完,再來看回退策略代碼就更簡單了,看一個固定時長的回退策略實現:

public class FixedBackOffPolicy extends StatelessBackOffPolicy implements SleepingBackOffPolicy<FixedBackOffPolicy> {
    // 默認使用 Thread.sleep等待
	private Sleeper sleeper = new ThreadWaitSleeper();

    // 可以指定 org.springframework.retry.backoff.ObjectWaitSleeper
	public void setSleeper(Sleeper sleeper) {
		this.sleeper = sleeper;
	}

	protected void doBackOff() throws BackOffInterruptedException {
		try {
			sleeper.sleep(backOffPeriod);
		}
		catch (InterruptedException e) {
			throw new BackOffInterruptedException("Thread interrupted while sleeping", e);
		}
	}
}

    默認的就是 Thread.sleep 一段時間。  

 

    至此,編程式的源碼簡單的流程已經瞭解了。接下來分析下註解的實現原理,可預見的,註解也藉助了編程式這套模板。

    大體邏輯主要分爲應用啓動時,對全局內 @Retryable方法的收集,以及運行期的動態代理。

@Configuration
public class RetryConfiguration extends AbstractPointcutAdvisor implements IntroductionAdvisor, BeanFactoryAware {


	@PostConstruct
	public void init() {
		Set<Class<? extends Annotation>> retryableAnnotationTypes = new LinkedHashSet<Class<? extends Annotation>>(1);
		retryableAnnotationTypes.add(Retryable.class);
        // 切點定義,也就是所有被 @Retryable標識的方法
		this.pointcut = buildPointcut(retryableAnnotationTypes);
        // 切面構建
		this.advice = buildAdvice();
		if (this.advice instanceof BeanFactoryAware) {
			((BeanFactoryAware) this.advice).setBeanFactory(beanFactory);
		}
	}

	protected Advice buildAdvice() {
        // 看這個實現
		AnnotationAwareRetryOperationsInterceptor interceptor = new AnnotationAwareRetryOperationsInterceptor();
		if (retryContextCache != null) {
			interceptor.setRetryContextCache(retryContextCache);
		}
		if (retryListeners != null) {
			interceptor.setListeners(retryListeners);
		}
		if (methodArgumentsKeyGenerator != null) {
			interceptor.setKeyGenerator(methodArgumentsKeyGenerator);
		}
		if (newMethodArgumentsIdentifier != null) {
			interceptor.setNewItemIdentifier(newMethodArgumentsIdentifier);
		}
		if (sleeper != null) {
			interceptor.setSleeper(sleeper);
		}
		return interceptor;
	}
}

    上面配置類,init 生命中期內定義了切點和切面,切點很簡單,就是所有 @Retryable 標識的方法;切面的話需要移步 AnnotationAwareRetryOperationsInterceptor

public class AnnotationAwareRetryOperationsInterceptor implements IntroductionInterceptor, BeanFactoryAware {

	@Override
	public Object invoke(MethodInvocation invocation) throws Throwable {
        // 獲取切面代理
		MethodInterceptor delegate = getDelegate(invocation.getThis(), invocation.getMethod());
		if (delegate != null) {
            // 執行代理方法
			return delegate.invoke(invocation);
		}
		else {
			return invocation.proceed();
		}
	}
}

    這裏切面的構建並不是我們關心的,所以 getDelegate 感興趣的可以自行研究,裏面加了一道緩存,保證代理對象只會創建一次。

    繼續跟源碼,可以找到代理切面實現類根據有無狀態有兩種(根據 @Retryable#stateful() 設置):

  •  org.springframework.retry.interceptor.RetryOperationsInterceptor
  • org.springframework.retry.interceptor.StatefulRetryOperationsInterceptor  (@CircuitBreaker強制是這種
public class RetryOperationsInterceptor implements MethodInterceptor {

    private RetryOperations retryOperations = new RetryTemplate();

    public Object invoke(final MethodInvocation invocation) throws Throwable {

        String name;
        if (StringUtils.hasText(label)) {
            name = label;
        } else {
            name = invocation.getMethod().toGenericString();
        }
        final String label = name;
        // 構建 org.springframework.retry.RetryCallback
        RetryCallback<Object, Throwable> retryCallback = new MethodInvocationRetryCallback<Object, Throwable>(
                invocation, label) {

            @Override
            public Object doWithRetry(RetryContext context) throws Exception {

                context.setAttribute(RetryContext.NAME, label);

                if (invocation instanceof ProxyMethodInvocation) {
                    try {
                        // 回調業務代碼塊
                        return ((ProxyMethodInvocation) invocation).invocableClone().proceed();
                    } catch (Exception e) {
                        throw e;
                    } catch (Error e) {
                        throw e;
                    } catch (Throwable e) {
                        throw new IllegalStateException(e);
                    }
                } else {
                    throw new IllegalStateException(
                            "MethodInvocation of the wrong type detected - this should not happen with Spring AOP, "
                                    + "so please raise an issue if you see this exception");
                }
            }

        };

        // 根據是否有 @Recover 兜底/補償方法,調用模板的不同方法
        if (recoverer != null) {
            RetryOperationsInterceptor.ItemRecovererCallback recoveryCallback = new RetryOperationsInterceptor.ItemRecovererCallback(invocation.getArguments(), recoverer);
            return this.retryOperations.execute(retryCallback, recoveryCallback);
        }

        return this.retryOperations.execute(retryCallback);

    }

}
public class StatefulRetryOperationsInterceptor implements MethodInterceptor {

    private RetryOperations retryOperations;

    @Override
    public Object invoke(final MethodInvocation invocation) throws Throwable {

        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Executing proxied method in stateful retry: " + invocation.getStaticPart() + "("
                    + ObjectUtils.getIdentityHexString(invocation) + ")");
        }

        Object[] args = invocation.getArguments();
        Object defaultKey = Arrays.asList(args);
        if (args.length == 1) {
            defaultKey = args[0];
        }

        Object key = createKey(invocation, defaultKey);
        RetryState retryState = new DefaultRetryState(key,
                this.newMethodArgumentsIdentifier != null && this.newMethodArgumentsIdentifier.isNew(args),
                this.rollbackClassifier);

        Object result = this.retryOperations.execute(new StatefulRetryOperationsInterceptor.StatefulMethodInvocationRetryCallback(invocation, label),
                this.recoverer != null ? new StatefulRetryOperationsInterceptor.ItemRecovererCallback(args, this.recoverer) : null, retryState);

        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Exiting proxied method in stateful retry with result: (" + result + ")");
        }

        return result;
    }
}

    最終 invoke 的實現裏面,可以看到調用模板方法。利用切面,省得每個方法都寫一遍 execute 方法。    

 

總結

    總之,重試框架並不複雜,已經有現成的工具,就不要重複造輪子。本文也是通過閱讀源碼後寫出來的,如果有理解不正確的地方,歡迎各位指正。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章