從零搭建腳手架之集成Spring?Retry實(shí)現(xiàn)失敗重試和熔斷器模式(實(shí)戰(zhàn)教程)
背景
在我們的大多數(shù)項(xiàng)目中,會(huì)有一些場(chǎng)景需要重試操作,而不是立即失敗,讓系統(tǒng)更加健壯且不易發(fā)生故障。
場(chǎng)景如下:
- 瞬時(shí)網(wǎng)絡(luò)抖動(dòng)故障
- 服務(wù)器重啟
- 偶發(fā)死鎖
- 某些上游的異?;蛘唔憫?yīng)碼,需要進(jìn)行重試
- 遠(yuǎn)程調(diào)用
- 從數(shù)據(jù)庫(kù)中獲取或存儲(chǔ)數(shù)據(jù)
以上皆為瞬時(shí)故障。
也會(huì)有一些場(chǎng)景,例如不是瞬時(shí)故障,例如接口響應(yīng)一直很慢,需要的是斷路器,如果還是繼續(xù)重試,會(huì)對(duì)服務(wù)有很大的影響,例如請(qǐng)求一次需要30s,如果還去不斷的重試,會(huì)拖垮我們的系統(tǒng),我們需要一定次數(shù)的失敗后停止向服務(wù)發(fā)送進(jìn)一步的請(qǐng)求,并在一段時(shí)間后恢復(fù)發(fā)送請(qǐng)求。
Spring Retry提供了以下能力:
- 失敗重試
- 斷路器模式
不支持艙壁bulkhead線程隔離
不支持超時(shí)timeout機(jī)制
項(xiàng)目地址:
https://github.com/spring-projects/spring-retry
https://docs.spring.io/spring-batch/docs/current/reference/html/retry.html
實(shí)戰(zhàn)
添加依賴
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>1.3.3</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${version}</version>
</dependency>
或者
<dependency>
<groupId>org.springframework.boot</groupId>
<aifactId>spring-boot-starter-aop</artifactId>
</dependency>啟用重試
@Configuration
@EnableRetry
public class RetryConfig {
}@Retryable
在需要重試的方法上加上@Retryable注解
部分參數(shù)如下:
- label: 重試的名字,系統(tǒng)唯一,默認(rèn) “”
- maxAttempts:異常時(shí)重試次數(shù),默認(rèn) 3
- maxAttemptsExpression: SpEL表達(dá)式 ,從配置文件獲取maxAttempts的值,可以在application.yml設(shè)置,與maxAttempts二選一
- exceptionExpression: SpEL表達(dá)式,匹配異常。例如:exceptionExpression = "#{message.contains('test')}"
- include:需要重試的異常
- exclude:不需要重試的異常
- backoff:重試中的退避策略 ,@Backoff注解,部分參數(shù)如下:
- value: 重試間隔ms,默認(rèn) 1000
- delay: 在指數(shù)情況下用作初始值,在均勻情況下用作最小值, 它與value屬性不能共存,當(dāng)delay不設(shè)置的時(shí)候會(huì)去讀value屬性設(shè)置的值,如果delay設(shè)置的話則會(huì)忽略value屬性, 默認(rèn) 0
- delayExpression: SpEL表達(dá)式 ,從配置文件獲取delay的值,可以在application.yml設(shè)置,與delay二選一
- multiplier: 則用作產(chǎn)生下一個(gè)退避延遲的乘數(shù) , 默認(rèn) 0
- delay = 2000, multiplier = 2 表示第一次重試間隔為2s,第二次為4秒,第三次為8s
- maxDelay: 最大的重試間隔,當(dāng)超過這個(gè)最大的重試間隔的時(shí)候,重試的間隔就等于maxDelay的值 默認(rèn) 0
@Service
@Slf4j
public class RetryService {
@Retryable(value = RuntimeException.class)
public void test(String param){
log.info(param);
throw new RuntimeException("laker Error");
}
}當(dāng)拋出
RuntimeException時(shí)會(huì)嘗試重試。根據(jù)
@Retryable的默認(rèn)行為,重試最多可能發(fā)生 3 次,重試之間有 1 秒的延遲。
測(cè)試日志如下:
2022-07-16 18:23:46.274 INFO 10204 --- [ main] com.example.demo.retry.RetryService : laker
2022-07-16 18:23:47.278 INFO 10204 --- [ main] com.example.demo.retry.RetryService : laker
2022-07-16 18:23:48.289 INFO 10204 --- [ main] com.example.demo.retry.RetryService : lakerjava.lang.RuntimeException: laker Error
at com.example.demo.retry.RetryService.test(RetryService.java:18)
at com.example.demo.retry.RetryService$$FastClassBySpringCGLIB$$41aa3d8d.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
@Recover
當(dāng)@Retryable方法重試失敗之后,最后就會(huì)調(diào)用@Recover方法。用于@Retryable失敗時(shí)的兜底處理方法。
@Recover的方法必須要與@Retryable注解的方法保持一致,第一入?yún)橐卦嚨漠惓?,其他參?shù)與@Retryable保持一致,返回值也要一樣,否則無(wú)法執(zhí)行!,方法可以是public、private.
@Service
@Slf4j
public class RetryService {
@Retryable(value = RuntimeException.class)
public void test(String param) {
log.info(param);
throw new RuntimeException("laker Error");
}
@Recover
void recover(RuntimeException e, String param) {
log.info("recover e:{},param:{}", e, param);
}
}在這里,當(dāng)拋出
RuntimeException時(shí)會(huì)嘗試重試。
test方法在 3 次嘗試后不斷拋出RuntimeException,則會(huì)調(diào)用recover()方法。
測(cè)試日志如下:
2022-07-16 18:40:19.828 INFO 4308 --- [ main] com.example.demo.retry.RetryService : laker
2022-07-16 18:40:20.834 INFO 4308 --- [ main] com.example.demo.retry.RetryService : laker
2022-07-16 18:40:21.848 INFO 4308 --- [ main] com.example.demo.retry.RetryService : laker
2022-07-16 18:40:21.849 INFO 4308 --- [ main] com.example.demo.retry.RetryService : recover e:java.lang.RuntimeException: laker Error,param:laker
@CircuitBreaker
熔斷模式:指在具體的重試機(jī)制下失敗后打開斷路器,過了一段時(shí)間,斷路器進(jìn)入半開狀態(tài),允許一個(gè)進(jìn)入重試,若失敗再次進(jìn)入斷路器,成功則關(guān)閉斷路器,注解為@CircuitBreaker,具體包括熔斷打開時(shí)間、重置過期時(shí)間。
同一個(gè)方法上與
@Retryable注解只能二選一,否則注解失效相關(guān)代碼參見
CircuitBreakerRetryPolicy.java
主要參數(shù)如下:
- maxAttempts: 最大嘗試次數(shù)(包括第一次失?。?,默認(rèn)為 3
- maxAttemptsExpression: SpEL表達(dá)式 ,從配置文件獲取maxAttempts的值,可以在application.yml設(shè)置,與maxAttempts二選一
- openTimeout:當(dāng)在此超時(shí)時(shí)間內(nèi)達(dá)到maxAttempts失敗時(shí),電路會(huì)自動(dòng)打開,防止訪問下游組件。默認(rèn)為 5000
- openTimeoutExpression: SpEL表達(dá)式
- resetTimeout: 如果電路打開的時(shí)間超過此超時(shí)時(shí)間,則它會(huì)在下一次調(diào)用時(shí)重置,以使下游組件有機(jī)會(huì)再次響應(yīng)。默認(rèn)為 20000
- resetTimeoutExpression: SpEL表達(dá)式
- label:短路器的名字,系統(tǒng)唯一
- include:需要短路的異常
- exclude:不需要短路的異常
@CircuitBreaker(maxAttempts = 2, openTimeout = 1000, resetTimeout = 2000, value = RuntimeException.class)
public void testCircuitBreaker(String param) {
log.info(param);
throw new RuntimeException("laker Error");
}
@Recover
void recover(RuntimeException e, String param) {
log.info("recover e:{},param:{}", e, param);
}當(dāng)拋出
RuntimeException時(shí)會(huì)嘗試熔斷。在openTimeout 1s時(shí)間內(nèi),觸發(fā)異常超過2次,斷路器打開,testCircuitBreaker業(yè)務(wù)方法不允許執(zhí)行,直接執(zhí)行恢復(fù)方法recover。
經(jīng)過resetTimeout 2s后,熔斷器關(guān)閉,繼續(xù)執(zhí)行testCircuitBreaker業(yè)務(wù)方法。
注意:這里沒有上面
@Retryable的能力了哦,但是這個(gè)實(shí)際項(xiàng)目還是很需要的。
測(cè)試日志如下:
2022-07-16 19:22:26.195 laker0
2022-07-16 19:22:26.195 recover e:java.lang.RuntimeException: laker Error,param:laker0
2022-07-16 19:22:26.196 laker1
2022-07-16 19:22:26.196 recover e:java.lang.RuntimeException: laker Error,param:laker1
2022-07-16 19:22:26.196 recover e:java.lang.RuntimeException: laker Error,param:laker2
2022-07-16 19:22:26.197 recover e:java.lang.RuntimeException: laker Error,param:laker3
2022-07-16 19:22:26.197 recover e:java.lang.RuntimeException: laker Error,param:laker4
2022-07-16 19:22:26.197 recover e:java.lang.RuntimeException: laker Error,param:laker5
2022-07-16 19:22:26.197 recover e:java.lang.RuntimeException: laker Error,param:laker6
2022-07-16 19:22:26.197 recover e:java.lang.RuntimeException: laker Error,param:laker7
2022-07-16 19:22:26.197 recover e:java.lang.RuntimeException: laker Error,param:laker8
2022-07-16 19:22:26.197 recover e:java.lang.RuntimeException: laker Error,param:laker9
2022-07-16 19:22:32.206 laker3
2022-07-16 19:22:32.206 recover e:java.lang.RuntimeException: laker Error,param:laker0
高級(jí)實(shí)戰(zhàn)
上面說(shuō)到了,斷路器@CircuitBreaker 并么有攜帶重試功能,所有我們實(shí)際項(xiàng)目要結(jié)合2者使用。
方式一 @CircuitBreaker + RetryTemplate
1.自定義RetryTemplate
@Configuration
@EnableRetry
public class RetryConfig {
@Bean
public RetryTemplate retryTemplate() {
RetryTemplate retryTemplate = new RetryTemplate();
FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
// 退避策略 因?yàn)槭撬矔r(shí)異常 所以不宜過大,100ms即可
fixedBackOffPolicy.setBackOffPeriod(100L);
retryTemplate.setBackOffPolicy(fixedBackOffPolicy);
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
// 重試3次
retryPolicy.setMaxAttempts(3);
retryTemplate.setRetryPolicy(retryPolicy);
return retryTemplate;
}
}
2.在斷路器中用retryTemplate包裹一層
@CircuitBreaker(maxAttempts = 2, openTimeout = 1000, resetTimeout = 2000, value = RuntimeException.class)
public String testCircuitBreaker(String param) {
return retryTemplate.execute(context -> {
log.info(String.format("Retry count %d", context.getRetryCount()) + param);
throw new RuntimeException("laker Error");
});
}
@Recover
String recover(RuntimeException e, String param) {
log.info("recover e:{},param:{}", e, param);
return "";
}測(cè)試日志如下:
2022-07-16 20:14:11.385 Retry count 0laker0
2022-07-16 20:14:11.496 Retry count 1laker0
2022-07-16 20:14:11.606 Retry count 2laker0
2022-07-16 20:14:11.607 recover e:java.lang.RuntimeException: laker Error,param:laker0
2022-07-16 20:14:11.608 Retry count 0laker1
2022-07-16 20:14:11.714 Retry count 1laker1
2022-07-16 20:14:11.826 Retry count 2laker1
2022-07-16 20:14:11.826 recover e:java.lang.RuntimeException: laker Error,param:laker1
2022-07-16 20:14:11.827 recover e:java.lang.RuntimeException: laker Error,param:laker2
2022-07-16 20:14:11.827 recover e:java.lang.RuntimeException: laker Error,param:laker3
2022-07-16 20:14:11.827 recover e:java.lang.RuntimeException: laker Error,param:laker4
2022-07-16 20:14:11.827 recover e:java.lang.RuntimeException: laker Error,param:laker5
2022-07-16 20:14:11.827 recover e:java.lang.RuntimeException: laker Error,param:laker6
2022-07-16 20:14:11.827 recover e:java.lang.RuntimeException: laker Error,param:laker7
2022-07-16 20:14:11.827 recover e:java.lang.RuntimeException: laker Error,param:laker8
2022-07-16 20:14:11.827 recover e:java.lang.RuntimeException: laker Error,param:laker9
方式二 @CircuitBreaker + @Retryable
定義2個(gè)springBean,一個(gè)用于重試,一個(gè)用于熔斷,且是熔斷包含著重試,否則會(huì)失效。
@Service
@Slf4j
public class RetryService {
@Autowired
RetryTemplate retryTemplate;
@Retryable(value = RuntimeException.class,backoff = @Backoff(delay = 100))
public void test(String param) {
log.info(param);
throw new RuntimeException("laker Error");
}
}
@Service
@Slf4j
public class CircuitBreakerService {
@Autowired
RetryService retryService;
@CircuitBreaker(maxAttempts = 2, openTimeout = 1000, resetTimeout = 2000, value = RuntimeException.class)
public void testCircuitBreaker(String param) {
// 這里是添加了重試注解的方法
retryService.test(param);
}
@Recover
void recover(RuntimeException e, String param) {
log.info("recover e:{},param:{}", e, param);
}
}
參考
https://blog.csdn.net/cckevincyh/article/details/112347200
到此這篇關(guān)于從零搭建開發(fā)腳手架之集成Spring Retry實(shí)現(xiàn)失敗重試和熔斷器模式的文章就介紹到這了,更多相關(guān)Spring Retry重試和熔斷器內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java8函數(shù)式編程應(yīng)用小結(jié)
Java8非常重要的就是引入了函數(shù)式編程的思想,使得這門經(jīng)典的面向?qū)ο笳Z(yǔ)言有了函數(shù)式的編程方式,彌補(bǔ)了很大程度上的不足,函數(shù)式思想在處理復(fù)雜問題上有著更為令人稱贊的特性,本文給大家介紹Java8函數(shù)式編程應(yīng)用小結(jié),感興趣的朋友一起看看吧2023-12-12
如何把springboot jar項(xiàng)目 改為war項(xiàng)目
這篇文章主要介紹了如何把springboot jar項(xiàng)目 改為war項(xiàng)目,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-11-11
SpringBoot啟動(dòng)security后如何關(guān)閉彈出的/login頁(yè)面
這篇文章主要介紹了SpringBoot啟動(dòng)security后如何關(guān)閉彈出的login頁(yè)面問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-12-12
SpringBoot項(xiàng)目中新增脫敏功能的實(shí)例代碼
項(xiàng)目中,由于使用端有兩個(gè),對(duì)于兩個(gè)端的數(shù)據(jù)權(quán)限并不一樣。Web端可以查看所有數(shù)據(jù),小程序端只能查看脫敏后的數(shù)據(jù),這篇文章主要介紹了SpringBoot項(xiàng)目中新增脫敏功能,需要的朋友可以參考下2022-11-11
SpringSecurity自定義登錄接口的實(shí)現(xiàn)
本文介紹了使用Spring Security實(shí)現(xiàn)自定義登錄接口,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2025-01-01
解決try-catch捕獲異常信息后Spring事務(wù)失效的問題
這篇文章主要介紹了解決try-catch捕獲異常信息后Spring事務(wù)失效的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06
Java數(shù)據(jù)結(jié)構(gòu)之散列表(動(dòng)力節(jié)點(diǎn)Java學(xué)院整理)
散列表(Hash table,也叫哈希表),是根據(jù)關(guān)鍵字(key value)而直接進(jìn)行訪問的數(shù)據(jù)結(jié)構(gòu)。這篇文章給大家介紹了java數(shù)據(jù)結(jié)構(gòu)之散列表,包括基本概念和散列函數(shù)相關(guān)知識(shí),需要的的朋友參考下吧2017-04-04

