- 理解重試機制
- 總結重試機制使用場景
- spring-retry重試組件
- 手寫一個基於註解的重試組件
- 重試機制下會出現的問題
- 模板方法設計模式實現異步重試機制
如果有,請轉給我!
1. 理解重試機制
“重試是爲了提高成功的可能性“
反過來理解,任何可能失敗且允許重試操作的場景,就適合使用重試機制。但有了重試機制就一定能成功嗎?顯然不是。如果不成功就一直重試,這種處理方式會使得業務線程一直被重試佔用,這樣會導致服務的負載線程暴增直至服務宕機,因此需要限制重試次數。失敗情況下,我們需要做後續的操作,如果是數據庫操作的重試,需要回滾事物;如果是服務調用的重試,需要郵件報警通知運維開發人員,恢復服務。
對於服務接口調用,可能是因爲網絡波動導致超時失敗,這時候所有重試次數是在很短時間內發起的話,就很容易全部超時失敗,因此超時機制還需要引入重試動作之間時間間隔以及第一次失敗後延遲多長時間再開始重試等機制。
重試機制要素
限制重試次數
每次重試的時間間隔
最終失敗結果的報警或事物回滾
在特定失敗異常事件情況下選擇重試
2. 總結重試機制使用場景
任何可能失敗且允許重試操作的場景,就適合使用重試機制。那麼在分佈式系統開發環境中,哪些場景需要是使用重試機制呢。
樂觀鎖機制保證數據安全的數據更新場景,如賬戶信息的金額數據更新。
微服務的分佈式架構下,服務的調用因超時而失敗。
3. spring-retry重試組件
spring-retry核心:配置重試元數據,失敗恢復或報警通知。
pom文件依賴
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
配置重試元數據
@Override
@Retryable(value = Exception.class,maxAttempts = 3 , backoff = @Backoff(delay = 2000,multiplier = 1.5))
public int retryServiceOne(int code) throws Exception {
// TODO Auto-generated method stub
System.out.println("retryServiceOne被調用,時間:"+LocalTime.now());
System.out.println("執行當前業務邏輯的線程名:"+Thread.currentThread().getName());
if (code==0){
throw new Exception("業務執行失敗情況!");
}
System.out.println("retryServiceOne執行成功!");
return 200;
}
配置元數據情況:
重試次數爲3
第一次重試延遲2s
每次重試時間間隔是前一次1.5倍
Exception類異常情況下重試
測試:
啓動應用,瀏覽器輸入:http://localhost:8080/springRetry。
後臺結果:
執行業務發起邏輯的線程名:http-nio-8080-exec-6
retryServiceOne被調用,時間:17:55:48.235
執行當前業務邏輯的線程名:http-nio-8080-exec-6
retryServiceOne被調用,時間:17:55:50.235
執行當前業務邏輯的線程名:http-nio-8080-exec-6
retryServiceOne被調用,時間:17:55:53.236
執行當前業務邏輯的線程名:http-nio-8080-exec-6
回調方法執行!!!!
4. 手寫一個基於註解的重試組件
註解類:
註解類包含的元數據有:
- 嘗試次數
- 重試間隔時間
- 拋出哪種異常會重試
- 重試完後還是失敗的恢復類
使用spring AOP技術,實現重試註解的切面邏輯類RetryAspect。
@Transactional(rollbackFor = Exception.class)
@Around("@annotation(jdkRetry)")
//開發自定義註解的時候,定要注意 @annotation(jdkRetry)和下面方法的參數,按規定是固定的形式的,否則報錯
public Object doConcurrentOperation(ProceedingJoinPoint pjp , JdkRetry jdkRetry) throws Throwable {
//獲取註解的屬性
// pjp.getClass().getMethod(, parameterTypes)
System.out.println("切面作用:"+jdkRetry.maxAttempts()+ " 恢復策略類:"+ jdkRetry.recoverServiceName());
Object service = JdkApplicationContext.jdkApplicationContext.getBean(jdkRetry.recoverServiceName());
Recover recover = null;
if(service == null)
return new Exception("recover處理服務實例不存在");
recover = (Recover)service;
long waitTime = jdkRetry.waitTime();
maxRetries = jdkRetry.maxAttempts();
Class<?> exceptionClass = jdkRetry.exception();
int numAttempts = 0;
do {
numAttempts++;
try {
//再次執行業務代碼
return pjp.proceed();
} catch (Exception ex) {
//必須只是樂觀鎖更新才能進行重試邏輯
System.out.println(ex.getClass().getName());
if(!ex.getClass().getName().equals(exceptionClass.getName()))
throw ex;
if (numAttempts > maxRetries) {
recover.recover(null);
//log failure information, and throw exception
// 如果大於 默認的重試機制 次數,我們這回就真正的拋出去了
// throw new Exception("重試邏輯執行完成,業務還是失敗!");
}else{
//如果 沒達到最大的重試次數,將再次執行
System.out.println("=====正在重試====="+numAttempts+"次");
TimeUnit.MILLISECONDS.sleep(waitTime);
}
}
} while (numAttempts <= this.maxRetries);
return 500;
}
切面類獲取到重試註解元信息後,切面邏輯會做以下相應的處理:
捕捉異常,對比該異常是否應該重試
統計重試次數,判斷是否超限
重試多次後失敗,執行失敗恢復邏輯或報警通知
測試:
啓動應用,瀏覽器輸入:http://localhost:8080/testAnnotationRetry
結果:
切面作用:3 恢復策略類:DefaultRecoverImpl
AnnotationServiceImpl被調用,時間:18:11:25.748
org.jackdking.retry.jdkdkingannotation.retryException.UpdateRetryException
=====正在重試=====1次
AnnotationServiceImpl被調用,時間:18:11:28.748
org.jackdking.retry.jdkdkingannotation.retryException.UpdateRetryException
=====正在重試=====2次
AnnotationServiceImpl被調用,時間:18:11:31.749
org.jackdking.retry.jdkdkingannotation.retryException.UpdateRetryException
=====正在重試=====3次
AnnotationServiceImpl被調用,時間:18:11:34.749
org.jackdking.retry.jdkdkingannotation.retryException.UpdateRetryException
2020-05-26 18:11:34.749 ERROR 14892 --- [io-8080-exec-10] o.j.r.j.recover.impl.DefaultRecoverImpl : 重試失敗,未進行任何補全,此爲默認補全:打出錯誤日誌
5. 重試機制下會出現的問題
冪等性問題:
在分佈式架構下,服務之間調用會因爲網絡原因出現超時失敗情況,而重試機制會重複多次調用服務,但是對於被調用放,就可能收到了多次調用。如果被調用方不具有天生的冪等性,那就需要增加服務調用的判重模塊,並對每次調用都添加一個唯一的id。
大量請求超時堆積:
超高併發下,大量的請求如果都進行超時重試的話,如果你的重試時間設置不安全的話,會導致大量的請求佔用服務器線程進行重試,這時候服務器線程負載就會暴增,導致服務器宕機。對於這種超高併發下的重試設計,我們不能讓重試放在業務線程,而是統一由異步任務來執行。
6. 模板方法設計模式實現異步重試機制
模板方法設計模式來實現異步重試機制
所有業務類繼承重試模板類RetryTemplate
@Service("serviceone")
public class RetryTemplateImpl extends RetryTemplate{
public RetryTemplateImpl() {
// TODO Auto-generated constructor stub
this.setRecover(new RecoverImpl());
}
@Override
protected Object doBiz() throws Exception {
// TODO Auto-generated method stub
int code = 0;
System.out.println("RetryTemplateImpl被調用,時間:"+LocalTime.now());
if (code==0){
throw new Exception("業務執行失敗情況!");
}
System.out.println("RetryTemplateImpl執行成功!");
return 200;
}
class RecoverImpl implements Recover{
@Override
public String recover() {
// TODO Auto-generated method stub
System.out.println("重試失敗 恢復邏輯,記錄日誌等操作");
return null;
}
}
}
業務實現類在doBiz方法內實現業務過程
所有業務實現一個恢復類,實現Recover接口,重試多次失敗後執行恢復邏輯
測試:
啓動應用,瀏覽器輸入:http://localhost:8080/testRetryTemplate
結果:
2020-05-26 22:53:41.935 INFO 25208 --- [nio-8080-exec-4] o.j.r.r.c.RetryTemplateController : 開始執行業務
RetryTemplateImpl被調用,時間:22:53:41.936
RetryTemplateImpl被調用,時間:22:53:41.938
RetryTemplateImpl被調用,時間:22:53:44.939
RetryTemplateImpl被調用,時間:22:53:47.939
2020-05-26 22:53:50.940 INFO 25208 --- [pool-1-thread-1] o.j.r.r.service.RetryTemplate : 業務邏輯失敗,重試結束
重試失敗 恢復邏輯,記錄日誌等操作
完整的demo項目,請關注公衆號“前沿科技bot“併發送"重試機制"獲取。