Spring Boot實現第一次啟動時自動初始化數據庫流程詳解

隨著互聯網的發展項目中的業務功能越來越復雜,有一些基礎服務我們不可避免的會去調用一些第三方的接口或者公司內其他項目中提供的服務,但是遠程服務的健壯性和網絡穩定性都是不可控因素。

在測試階段可能沒有什么異常情況,但上線后可能會出現調用的接口因為內部錯誤或者網絡波動而出錯或返回系統異常,因此我們必須考慮加上重試機制

重試機制 可以提高系統的健壯性,并且減少因網絡波動依賴服務臨時不可用帶來的影響,讓系統能更穩定的運行

1. 手動重試

手動重試:使用 while 語句進行重試:

@Service
public class OrderServiceImpl implements OrderService {public void addOrder() {int times = 1;while (times <= 5) {try {// 故意拋異常int i = 3 / 0;// addOrder} catch (Exception e) {System.out.println("重試" + times + "次");Thread.sleep(2000);times++;if (times > 5) {throw new RuntimeException("不再重試!");}}}}
}

運行上述代碼:

圖片

上述代碼看上去可以解決重試問題,但實際上存在一些弊端:

  1. 由于沒有重試間隔,很可能遠程調用的服務還沒有從網絡異常中恢復,所以有可能接下來的幾次調用都會失敗
  2. 代碼侵入式太高,調用方代碼不夠優雅
  3. 項目中遠程調用的服務可能有很多,每個都去添加重試會出現大量的重復代碼

2. 靜態代理

上面的處理方式由于需要對業務代碼進行大量修改,雖然實現了功能,但是對原有代碼的侵入性太強,可維護性差。所以需要使用一種更優雅一點的方式,不直接修改業務代碼,那要怎么做呢?

其實很簡單,直接在業務代碼的外面再包一層就行了,代理模式在這里就有用武之地了。

@Service
public class OrderServiceProxyImpl implements OrderService {@Autowiredprivate OrderServiceImpl orderService;@Overridepublic void addOrder() {int times = 1;while (times <= 5) {try {// 故意拋異常int i = 3 / 0;orderService.addOrder();} catch (Exception e) {System.out.println("重試" + times + "次");try {Thread.sleep(2000);} catch (InterruptedException ex) {ex.printStackTrace();}times++;if (times > 5) {throw new RuntimeException("不再重試!");}}}}
}

這樣,重試邏輯就都由代理類來完成,原業務類的邏輯就不需要修改了,以后想修改重試邏輯也只需要修改這個類就行了

代理模式雖然要更加優雅,但是如果依賴的服務很多的時候,要為每個服務都創建一個代理類,顯然過于麻煩,而且其實重試的邏輯都大同小異,無非就是重試的次數和延時不一樣而已。如果每個類都寫這么一長串類似的代碼,顯然,不優雅!

3. JDK 動態代理

這時候,動態代理就閃亮登場了。只需要寫一個代理處理類就 ok 了

public class RetryInvocationHandler implements InvocationHandler {private final Object subject;public RetryInvocationHandler(Object subject) {this.subject = subject;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {int times = 1;while (times <= 5) {try {// 故意拋異常int i = 3 / 0;return method.invoke(subject, args);} catch (Exception e) {System.out.println("重試【" + times + "】次");try {Thread.sleep(2000);} catch (InterruptedException ex) {ex.printStackTrace();}times++;if (times > 5) {throw new RuntimeException("不再重試!");}}}return null;}public static Object getProxy(Object realSubject) {InvocationHandler handler = new RetryInvocationHandler(realSubject);return Proxy.newProxyInstance(handler.getClass().getClassLoader(), realSubject.getClass().getInterfaces(), handler);}}

測試:

@RestController
@RequestMapping("/order")
public class OrderController {@Qualifier("orderServiceImpl")@Autowiredprivate OrderService orderService;@GetMapping("/addOrder")public String addOrder() {OrderService orderServiceProxy = (OrderService)RetryInvocationHandler.getProxy(orderService);orderServiceProxy.addOrder();return "addOrder";}}

動態代理可以將重試邏輯都放到一塊,顯然比直接使用代理類要方便很多,也更加優雅。

這里使用的是JDK動態代理,因此就存在一個天然的缺陷,如果想要被代理的類,沒有實現任何接口,那么就無法為其創建代理對象,這種方式就行不通了

4. CGLib 動態代理

既然已經說到了 JDK 動態代理,那就不得不提 CGLib 動態代理了。使用 JDK 動態代理對被代理的類有要求,不是所有的類都能被代理,而 CGLib 動態代理則剛好解決了這個問題

@Component
public class CGLibRetryProxyHandler implements MethodInterceptor {private Object target;@Overridepublic Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {int times = 1;while (times <= 5) {try {// 故意拋異常int i = 3 / 0;return method.invoke(target, objects);} catch (Exception e) {System.out.println("重試【" + times + "】次");try {Thread.sleep(2000);} catch (InterruptedException ex) {ex.printStackTrace();}times++;if (times > 5) {throw new RuntimeException("不再重試!");}}}return null;}public Object getCglibProxy(Object objectTarget){this.target = objectTarget;Enhancer enhancer = new Enhancer();enhancer.setSuperclass(objectTarget.getClass());enhancer.setCallback(this);Object result = enhancer.create();return result;}}

測試:

@GetMapping("/addOrder")
public String addOrder() {OrderService orderServiceProxy = (OrderService) cgLibRetryProxyHandler.getCglibProxy(orderService);orderServiceProxy.addOrder();return "addOrder";
}

這樣就很棒了,完美的解決了 JDK 動態代理帶來的缺陷。優雅指數上漲了不少。

但這個方案仍舊存在一個問題,那就是需要對原來的邏輯進行侵入式修改,在每個被代理實例被調用的地方都需要進行調整,這樣仍然會對原有代碼帶來較多修改

5. 手動 Aop

考慮到以后可能會有很多的方法也需要重試功能,咱們可以將重試這個共性功能通過 AOP 來實現:使用 AOP 來為目標調用設置切面,即可在目標方法調用前后添加一些重試的邏輯

<dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId>
</dependency>

自定義注解:

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyRetryable {// 最大重試次數int retryTimes() default 3;// 重試間隔int retryInterval() default 1;}
@Slf4j
@Aspect
@Component
public class RetryAspect {@Pointcut("@annotation(com.hcr.sbes.retry.annotation.MyRetryable)")private void retryMethodCall(){}@Around("retryMethodCall()")public Object retry(ProceedingJoinPoint joinPoint) throws InterruptedException {// 獲取重試次數和重試間隔MyRetryable retry = ((MethodSignature)joinPoint.getSignature()).getMethod().getAnnotation(MyRetryable.class);int maxRetryTimes = retry.retryTimes();int retryInterval = retry.retryInterval();Throwable error = new RuntimeException();for (int retryTimes = 1; retryTimes <= maxRetryTimes; retryTimes++){try {Object result = joinPoint.proceed();return result;} catch (Throwable throwable) {error = throwable;log.warn("調用發生異常,開始重試,retryTimes:{}", retryTimes);}Thread.sleep(retryInterval * 1000L);}throw new RuntimeException("重試次數耗盡", error);}}

給需要重試的方法添加注解 @MyRetryable

@Service
public class OrderServiceImpl implements OrderService {@Override@MyRetryable(retryTimes = 5, retryInterval = 2)public void addOrder() {int i = 3 / 0;// addOrder}}

這樣即不用編寫重復代碼,實現上也比較優雅了:一個注解就實現重試。

6. spring-retry

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

開啟重試功能:在啟動類或者配置類上添加 @EnableRetry 注解

在需要重試的方法上添加 @Retryable 注解

@Slf4j
@Service
public class OrderServiceImpl implements OrderService {@Override@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 2000, multiplier = 2))public void addOrder() {System.out.println("重試...");int i = 3 / 0;// addOrder}@Recoverpublic void recover(RuntimeException e) {log.error("達到最大重試次數", e);}}

該方法調用后會進行重試,最大重試次數為 3,第一次重試間隔為 2s,之后以 2 倍大小進行遞增,第二次重試間隔為 4 s,第三次為 8s

Spring 的重試機制還支持很多很有用的特性,由三個注解完成:

@Retryable
@Backoff
@Recover

查看 @Retryable 注解源碼:指定異常重試、次數

public @interface Retryable {// 設置重試攔截器的 bean 名稱String interceptor() default "";// 只對特定類型的異常進行重試。默認:所有異常Class<? extends Throwable>[] value() default {};// 包含或者排除哪些異常進行重試Class<? extends Throwable>[] include() default {};Class<? extends Throwable>[] exclude() default {};// l設置該重試的唯一標志,用于統計輸出String label() default "";boolean stateful() default false;// 最大重試次數,默認為 3 次int maxAttempts() default 3;String maxAttemptsExpression() default "";// 設置重試補償機制,可以設置重試間隔,并且支持設置重試延遲倍數Backoff backoff() default @Backoff;// 異常表達式,在拋出異常后執行,以判斷后續是否進行重試String exceptionExpression() default "";String[] listeners() default {};
}

@Backoff 注解: 指定重試回退策略(如果因為網絡波動導致調用失敗,立即重試可能還是會失敗,最優選擇是等待一小會兒再重試。決定等待多久之后再重試的方法。通俗的說,就是每次重試是立即重試還是等待一段時間后重試)

@Recover 注解: 進行善后工作:當重試達到指定次數之后,會調用指定的方法來進行日志記錄等操作

注意:

@Recover 注解標記的方法必須和被 @Retryable 標記的方法在同一個類中
重試方法拋出的異常類型需要與 recover()方法參數類型保持一致
recover() 方法返回值需要與重試方法返回值保證一致
recover() 方法中不能再拋出Exception,否則會報無法識別該異常的錯誤

這里還需要再提醒的一點是,由于 Spring Retry 用到了 Aspect 增強,所以就會有使用 Aspect 不可避免的坑——方法內部調用,如果被 @Retryable 注解的方法的調用方和被調用方處于同一個類中,那么重試將會失效

通過以上幾個簡單的配置,可以看到 Spring Retry 重試機制考慮的比較完善,比自己寫AOP實現要強大很多

弊端:
但也還是存在一定的不足,Spring的重試機制只支持對 異常 進行捕獲,而無法對返回值進行校驗

@Retryable
public String hello() {long current = count.incrementAndGet();System.out.println("第" + current +"次被調用");if (current % 3 != 0) {log.warn("調用失敗");return "error";}return "success";
}

因此就算在方法上添加 @Retryable,也無法實現失敗重試

除了使用注解外,Spring Retry 也支持直接在調用時使用代碼進行重試:

@Test
public void normalSpringRetry() {// 表示哪些異常需要重試,key表示異常的字節碼,value為true表示需要重試Map<Class<? extends Throwable>, Boolean> exceptionMap = new HashMap<>();exceptionMap.put(HelloRetryException.class, true);// 構建重試模板實例RetryTemplate retryTemplate = new RetryTemplate();// 設置重試回退操作策略,主要設置重試間隔時間FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();long fixedPeriodTime = 1000L;backOffPolicy.setBackOffPeriod(fixedPeriodTime);// 設置重試策略,主要設置重試次數int maxRetryTimes = 3;SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(maxRetryTimes, exceptionMap);retryTemplate.setRetryPolicy(retryPolicy);retryTemplate.setBackOffPolicy(backOffPolicy);Boolean execute = retryTemplate.execute(//RetryCallbackretryContext -> {String hello = helloService.hello();log.info("調用的結果:{}", hello);return true;},// RecoverCallBackretryContext -> {//RecoveryCallbacklog.info("已達到最大重試次數");return false;});
}

此時唯一的好處是可以設置多種重試策略:

NeverRetryPolicy:只允許調用RetryCallback一次,不允許重試
AlwaysRetryPolicy:允許無限重試,直到成功,此方式邏輯不當會導致死循環
SimpleRetryPolicy:固定次數重試策略,默認重試最大次數為3次,RetryTemplate默認使用的策略
TimeoutRetryPolicy:超時時間重試策略,默認超時時間為1秒,在指定的超時時間內允許重試
ExceptionClassifierRetryPolicy:設置不同異常的重試策略,類似組合重試策略,區別在于這里只區分不同異常的重試
CircuitBreakerRetryPolicy:有熔斷功能的重試策略,需設置3個參數openTimeout、resetTimeout和delegate
CompositeRetryPolicy:組合重試策略,有兩種組合方式,樂觀組合重試策略是指只要有一個策略允許即可以重試,悲觀組合重試策略是指只要有一個策略不允許即可以重試,但不管哪種組合方式,組合中的每一個策略都會執行

7. guava-retry

和 Spring Retry 相比,Guava Retry 具有更強的靈活性,并且能夠根據 返回值 來判斷是否需要重試

<dependency><groupId>com.github.rholder</groupId><artifactId>guava-retrying</artifactId><version>2.0.0</version>
</dependency>
@Override
public String guavaRetry(Integer num) {Retryer<String> retryer = RetryerBuilder.<String>newBuilder()//無論出現什么異常,都進行重試.retryIfException()//返回結果為 error時,進行重試.retryIfResult(result -> Objects.equals(result, "error"))//重試等待策略:等待 2s 后再進行重試.withWaitStrategy(WaitStrategies.fixedWait(2, TimeUnit.SECONDS))//重試停止策略:重試達到 3 次.withStopStrategy(StopStrategies.stopAfterAttempt(3)).withRetryListener(new RetryListener() {@Overridepublic <V> void onRetry(Attempt<V> attempt) {System.out.println("RetryListener: 第" + attempt.getAttemptNumber() + "次調用");}}).build();try {retryer.call(() -> testGuavaRetry(num));} catch (Exception e) {e.printStackTrace();}return "test";
}

先創建一個Retryer實例,然后使用這個實例對需要重試的方法進行調用,可以通過很多方法來設置重試機制:

retryIfException():對所有異常進行重試
retryIfRuntimeException():設置對指定異常進行重試
retryIfExceptionOfType():對所有 RuntimeException 進行重試
retryIfResult():對不符合預期的返回結果進行重試

還有五個以 withXxx 開頭的方法,用來對重試策略/等待策略/阻塞策略/單次任務執行時間限制/自定義監聽器進行設置,以實現更加強大的異常處理:

withRetryListener():設置重試監聽器,用來執行額外的處理工作
withWaitStrategy():重試等待策略
withStopStrategy():停止重試策略
withAttemptTimeLimiter:設置任務單次執行的時間限制,如果超時則拋出異常
withBlockStrategy():設置任務阻塞策略,即可以設置當前重試完成,下次重試開始前的這段時間做什么事情

總結

從手動重試,到使用 Spring AOP 自己動手實現,再到站在巨人肩上使用特別優秀的開源實現 Spring Retry 和 Google guava-retrying,經過對各種重試實現方式的介紹,可以看到以上幾種方式基本上已經滿足大部分場景的需要:

如果是基于 Spring 的項目,使用 Spring Retry 的注解方式已經可以解決大部分問題
如果項目沒有使用 Spring 相關框架,則適合使用 Google guava-retrying:自成體系,使用起來更加靈活強大

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/40494.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/40494.shtml
英文地址,請注明出處:http://en.pswp.cn/news/40494.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

https證書獲取的方法及好處

我們常說的https證書其實就是ssl證書&#xff0c;眼下為網站部署https證書是保障網站安全必不可少的一步。而https證書該如何獲取呢&#xff1f;下面就簡單介紹一下https證書獲取的方法。 https證書獲取途徑有兩種&#xff1a;自己簽發和由受信任的CA機構簽發。 自己給自己簽…

全國三網優惠話費充值接口開發指南

一、文檔綜述 近期想做項目的看過來~三網&#xff08;全國移動、聯通、電信&#xff09;話費、電費充值接口能夠實現將接口接入到小程序或者app上面&#xff0c;通過接口提交號碼和金額進行充值&#xff0c;可以幫助相關人員快速完成接口對接與聯調&#xff0c;平臺用戶可以通…

設計HTML5文本

網頁文本內容豐富、形式多樣&#xff0c;通過不同的版式顯示在頁面中&#xff0c;為用戶提供最直接、最豐富的信息。HTML5新增了很多文本標簽&#xff0c;它們都有特殊的語義&#xff0c;正確使用這些標簽&#xff0c;可以讓網頁文本更嚴謹、更符合語義。 1、通用文本 1.1、標…

算法競賽備賽之搜索與圖論訓練提升,暑期集訓營培訓

目錄 1.DFS和BFS 1.1.DFS深度優先搜索 1.2.BFS廣度優先搜索 2.樹與圖的遍歷&#xff1a;拓撲排序 3.最短路 3.1.迪杰斯特拉算法 3.2.貝爾曼算法 3.3.SPFA算法 3.4.多源匯最短路Floy算法 4.最小生成樹 4.1.普利姆算法 4.2.克魯斯卡爾算法 5.二分圖&#xff1a;染色法…

7. CSS(四)

目錄 一、浮動 &#xff08;一&#xff09;傳統網頁布局的三種方式 &#xff08;二&#xff09;標準流&#xff08;普通流/文檔流&#xff09; &#xff08;三&#xff09;為什么需要浮動&#xff1f; &#xff08;四&#xff09;什么是浮動 &#xff08;五&#xff09;浮…

OpenAI全球招外包大軍,手把手訓練ChatGPT取代碼農 ; 碼農:我自己「殺」自己

目錄 前言 OpenAI招了一千多名外包人員&#xff0c;訓練AI學會像人類一樣一步步思考。如果ChatGPT「學成歸來」&#xff0c;碼農恐怕真的危了&#xff1f; 碼農真的危了&#xff01; 當時OpenAI也說&#xff0c;ChatGPT最合適的定位&#xff0c;應該是編碼輔助工具。 用Cha…

常用的Elasticsearch查詢DSL

1.基本查詢 GET /index_name/_search {"query": {"match": {"dispatchClass": "1"}} }2.多條件查詢 GET /index_name/_search {"query": {"bool": {"must": [{"match": {"createUser&…

計算機競賽 opencv 圖像識別 指紋識別 - python

0 前言 &#x1f525; 優質競賽項目系列&#xff0c;今天要分享的是 &#x1f6a9; 基于機器視覺的指紋識別系統 &#x1f947;學長這里給一個題目綜合評分(每項滿分5分) 難度系數&#xff1a;3分工作量&#xff1a;3分創新點&#xff1a;4分 該項目較為新穎&#xff0c;適…

Vue引入Echarts報錯 import * as echarts from “echarts“;

項目場景&#xff1a; 已經下載好echarts cnpm i echarts Vue引入Echarts import echarts from echarts mounted() {this.myChart echarts.init(document.querySelector(.right))this.myChart.setOption({title: {text: 消費列表,left: center},...問題描述 原因分析&#…

【100天精通python】Day38:GUI界面編程_PyQT從入門到實戰(中)

目錄 專欄導讀 4 數據庫操作 4.1 連接數據庫 4.2 執行 SQL 查詢和更新&#xff1a; 4.3 使用模型和視圖顯示數據 5 多線程編程 5.1 多線程編程的概念和優勢 5.2 在 PyQt 中使用多線程 5.3 處理多線程間的同步和通信問題 5.3.1 信號槽機制 5.3.2 線程安全的數據訪問 Q…

日常BUG——通過命令行創建vue項目報錯

&#x1f61c;作 者&#xff1a;是江迪呀??本文關鍵詞&#xff1a;日常BUG、BUG、問題分析??每日 一言 &#xff1a;存在錯誤說明你在進步&#xff01; 一、問題描述 在使用vue命令行創建一個vue項目時&#xff0c;出現一下的錯誤&#xff1a; vue create my…

UDP數據報結構分析(面試重點)

在傳輸層中有UDP和TCP兩個重要的協議&#xff0c;下面將針對UDP數據報的結構進行分析 UDP結構圖示 UDP報頭結構的分析 UDP報頭有4個屬性&#xff0c;分別是源端口&#xff0c;目的端口&#xff0c;UDP報文長度&#xff0c;校驗和&#xff0c;它們都占16位2個字節&#xff0c;所…

.devos勒索病毒解密方法|勒索病毒解決|勒索病毒恢復|數據庫修復

導言&#xff1a; 隨著科技的迅猛發展&#xff0c;網絡安全問題也日益凸顯。近期&#xff0c;一種名為 .devos 的勒索病毒在網絡安全領域引起了廣泛的關注和警惕。本文91數據恢復將 探討如何解密被其加密的數據文件&#xff0c;并提供預防措施以避免受到類似威脅的侵害。 如不幸…

【java面向對象中static關鍵字】

提綱 static修飾成員變量static修飾成員變量的應用場景static修飾成員方法static修飾成員方法的應用場景static的注意事項static的應用知識&#xff1a;代碼塊static的應用知識&#xff1a;單例設計模式 static靜態的意思&#xff0c;可以修飾成員變量&#xff0c;成員方法&a…

FPGA_學習_14_第一個自寫模塊的感悟和ila在線調試教程與技巧(尋找APD的擊穿偏壓)

前一篇博客我們提到了&#xff0c;如果要使用算法找到Vbr&#xff0c;通過尋找APD采集信號的噪聲方差的劇變點去尋找Vbr是一個不錯的方式。此功能的第一步是在FPGA中實現方差的計算&#xff0c;這個我們已經在上一篇博客中實現了。 繼上一篇博客之后&#xff0c;感覺過了很久了…

【Image captioning】ruotianluo/self-critical.pytorch之1—數據集的加載與使用

【Image captioning】ruotianluo/self-critical.pytorch之1—數據集的加載與使用 作者&#xff1a;安靜到無聲 個人主頁 數據加載程序示意圖 使用方法 示例代碼 #%%from __future__ import absolute_import from __future__ import division from __future__ import print_…

Flink-網絡流控及反壓剖析

參考&#xff1a; Apache Flink學習網

開源,微信小程序 美食便簽地圖(FoodNoteMap)的設計與開發

目錄 0 前言 1 美食便簽地圖簡介 2 美食便簽地圖小程序端開發 2.1技術選型 2.2前端UI設計 2.3主頁界面 2.4個人信息界面 2.5 添加美食界面 2.6美食便簽界面 2.8 美食好友界面 2.9 美食圈子界面 2.10 子頁面-店鋪詳情界面 2.11 后臺數據緩存 2.12 訂閱消息通知 2.1…

Redis為什么能如此之快

推薦閱讀 AI文本 OCR識別最佳實踐 AI Gamma一鍵生成PPT工具直達鏈接 玩轉cloud Studio 在線編碼神器 玩轉 GPU AI繪畫、AI講話、翻譯,GPU點亮AI想象空間 資源分享 「java、python面試題」來自UC網盤app分享&#xff0c;打開手機app&#xff0c;額外獲得1T空間 https://dr…

“深入探索JVM內部機制:解密Java虛擬機原理“

標題&#xff1a;深入探索JVM內部機制&#xff1a;解密Java虛擬機原理 摘要&#xff1a;本文將深入探索Java虛擬機&#xff08;JVM&#xff09;的內部機制&#xff0c;揭示其工作原理和關鍵組成部分&#xff0c;包括類加載、內存管理、垃圾回收、即時編譯和運行時數據區域等。…