如果有,請轉給我!
“重試是為了提高成功的可能性“
反過來理解,任何可能失敗且允許重試操作的場景,就適合使用重試機制。但有了重試機制就一定能成功嗎?顯然不是。如果不成功就一直重試,這種處理方式會使得業務執行緒一直被重試佔用,這樣會導致服務的負載執行緒暴增直至服務當機,因此需要限制重試次數。失敗情況下,我們需要做後續的操作,如果是資料庫操作的重試,需要回滾事物;如果是服務呼叫的重試,需要郵件報警通知運維開發人員,恢復服務。
對於服務介面呼叫,可能是因為網路波動導致超時失敗,這時候所有重試次數是在很短時間內發起的話,就很容易全部超時失敗,因此超時機制還需要引入重試動作之間時間間隔以及第一次失敗後延遲多長時間再開始重試等機制。
重試機制要素
- 限制重試次數
- 每次重試的時間間隔
- 最終失敗結果的報警或事物回滾
- 在特定失敗異常事件情況下選擇重試
任何可能失敗且允許重試操作的場景,就適合使用重試機制。那麼在分散式系統開發環境中,哪些場景需要是使用重試機制呢。
- 樂觀鎖機制保證資料安全的資料更新場景,如賬戶資訊的金額資料更新。
- 微服務的分散式架構下,服務的呼叫因超時而失敗。
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 回撥方法執行!!!!
註解類:
/** * 重試註解 */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @Documented public @interface JdkRetry{ //預設 int maxAttempts() default 3; //預設每次間隔等待3000毫秒 long waitTime() default 3000; //捕捉到的異常型別 再進行重發 Class<?> exception () default Exception.class ; String recoverServiceName () default "DefaultRecoverImpl"; }
註解類包含的後設資料有:
- 嘗試次數
- 重試間隔時間
- 丟擲哪種異常會重試
- 重試完後還是失敗的恢復類
使用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 : 重試失敗,未進行任何補全,此為預設補全:打出錯誤日誌
冪等性問題:
在分散式架構下,服務之間呼叫會因為網路原因出現超時失敗情況,而重試機制會重複多次呼叫服務,但是對於被呼叫放,就可能收到了多次呼叫。如果被呼叫方不具有天生的冪等性,那就需要增加服務呼叫的判重模組,並對每次呼叫都新增一個唯一的id。
大量請求超時堆積:
超高併發下,大量的請求如果都進行超時重試的話,如果你的重試時間設定不安全的話,會導致大量的請求佔用伺服器執行緒進行重試,這時候伺服器執行緒負載就會暴增,導致伺服器當機。對於這種超高併發下的重試設計,我們不能讓重試放在業務執行緒,而是統一由非同步任務來執行。
模板方法設計模式來實現非同步重試機制
所有業務類繼承重試模板類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“併傳送"重試機制"獲取。