簡介
軟件架構從當初的單機,演變到後來的集羣,再到後來的分佈式應用。原本看似可以信任的服務調用,加上了網絡因素就變得不再可靠。再考慮到一些調用鏈路的特殊性,又要保證性能,又要儘可能增加成功率,所以調用方必須肩負起重試的責任。
自己寫,怎樣實現?
重試並不複雜,首先來分析下重試的調用場景,可以想到業務當中不止一處會需要重試能力,並且業務其實更關乎自己的代碼塊被重試就可以了,而不在乎如何實現的重試。
所以變化的是一段可以重複執行的代碼塊,以及重試次數等。
① 代碼塊可以用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 方法。
總結
總之,重試框架並不複雜,已經有現成的工具,就不要重複造輪子。本文也是通過閱讀源碼後寫出來的,如果有理解不正確的地方,歡迎各位指正。