如果有,請轉給我!
“重試是為了提高成功的可能性“
反過來理解,任何可能失敗且允許重試操作的場景,就適合使用重試機制。但有了重試機制就一定能成功嗎?顯然不是。如果不成功就一直重試,這種處理方式會使得業務線程一直被重試占用,這樣會導致服務的負載線程暴增直至服務宕機,因此需要限制重試次數。失敗情況下,我們需要做后續的操作,如果是數據庫操作的重試,需要回滾事物;如果是服務調用的重試,需要郵件報警通知運維開發人員,恢復服務。
對於服務接口調用,可能是因為網絡波動導致超時失敗,這時候所有重試次數是在很短時間內發起的話,就很容易全部超時失敗,因此超時機制還需要引入重試動作之間時間間隔以及第一次失敗后延遲多長時間再開始重試等機制。
重試機制要素
- 限制重試次數
- 每次重試的時間間隔
- 最終失敗結果的報警或事物回滾
- 在特定失敗異常事件情況下選擇重試
任何可能失敗且允許重試操作的場景,就適合使用重試機制。那么在分布式系統開發環境中,哪些場景需要是使用重試機制呢。
- 樂觀鎖機制保證數據安全的數據更新場景,如賬戶信息的金額數據更新。
- 微服務的分布式架構下,服務的調用因超時而失敗。
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 : 業務邏輯失敗,重試結束 重試失敗 恢復邏輯,記錄日志等操作
查看更多 “Java架構師方案” 系列文章 以及 SpringBoot2.0學習示例
如果大家覺得這篇文章對你學習架構有幫助的話,還請點贊,在看支持一下。github項目也記得點個星哦!
完整的demo項目,請關注公眾號“前沿科技bot“並發送"重試機制"獲取。