1 重試之Spring-Retry
1.1 簡(jiǎn)介
Spring Retry
是Spring
框架提供的一個(gè)模塊,它通過提供注解或編程方式的方式傲宜,幫助我們實(shí)現(xiàn)方法級(jí)別的重試機(jī)制运杭。在Spring Boot
中,可以很方便地集成并使用Spring Retry
1.2 直接使用
Spring Retry
為 Spring
應(yīng)用程序提供了聲明性重試支持蛋哭。它用于Spring
批處理县习、Spring
集成、Apache Hadoop
(等等)谆趾。它主要是針對(duì)可能拋出異常的一些調(diào)用操作躁愿,進(jìn)行有策略的重試
1.2.1 pom.xml
準(zhǔn)備工作
我們只需要加上依賴:
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>1.2.2.RELEASE</version>
</dependency>
1.2.2 重試任務(wù)
準(zhǔn)備一個(gè)任務(wù)方法,這里是采用一個(gè)隨機(jī)整數(shù)
沪蓬,根據(jù)不同的條件返回不同的值彤钟,或者拋出異常
package com.zgd.demo.thread.retry;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomUtils;
import org.springframework.remoting.RemoteAccessException;
/**
* @Description:
*/
@Slf4j
public class RetryDemoTask {
/**
* 重試方法
* @return
*/
public static boolean retryTask(String param) {
log.info("收到請(qǐng)求參數(shù):{}",param);
int i = RandomUtils.nextInt(0,11);
log.info("隨機(jī)生成的數(shù):{}",i);
if (i == 0) {
log.info("為0,拋出參數(shù)異常.");
throw new IllegalArgumentException("參數(shù)異常");
}else if (i == 1){
log.info("為1,返回true.");
return true;
}else if (i == 2){
log.info("為2,返回false.");
return false;
}else{
//為其他
log.info("大于2,拋出自定義異常.");
throw new RemoteAccessException("大于2,拋出遠(yuǎn)程訪問異常");
}
}
}
1.2.3 使用SpringRetryTemplate
這里可以寫我們的代碼了
package com.zgd.demo.thread.retry.spring;
import com.zgd.demo.thread.retry.RetryDemoTask;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.springframework.remoting.RemoteAccessException;
import org.springframework.retry.backoff.FixedBackOffPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
import java.util.HashMap;
import java.util.Map;
/**
* @Description: spring-retry 重試框架
*/
@Slf4j
public class SpringRetryTemplateTest {
/**
* 重試間隔時(shí)間ms,默認(rèn)1000ms
* */
private long fixedPeriodTime = 1000L;
/**
* 最大重試次數(shù),默認(rèn)為3
*/
private int maxRetryTimes = 3;
/**
* 表示哪些異常需要重試,key表示異常的字節(jié)碼,value為true表示需要重試
*/
private Map<Class<? extends Throwable>, Boolean> exceptionMap = new HashMap<>();
@Test
public void test() {
exceptionMap.put(RemoteAccessException.class,true);
// 構(gòu)建重試模板實(shí)例
RetryTemplate retryTemplate = new RetryTemplate();
// 設(shè)置重試回退操作策略,主要設(shè)置重試間隔時(shí)間
FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
backOffPolicy.setBackOffPeriod(fixedPeriodTime);
// 設(shè)置重試策略跷叉,主要設(shè)置重試次數(shù)
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(maxRetryTimes, exceptionMap);
retryTemplate.setRetryPolicy(retryPolicy);
retryTemplate.setBackOffPolicy(backOffPolicy);
Boolean execute = retryTemplate.execute(
//RetryCallback
retryContext -> {
boolean b = RetryDemoTask.retryTask("abc");
log.info("調(diào)用的結(jié)果:{}", b);
return b;
},
retryContext -> {
//RecoveryCallback
log.info("已達(dá)到最大重試次數(shù)或拋出了不重試的異常~~~");
return false;
}
);
log.info("執(zhí)行結(jié)果:{}",execute);
}
}
簡(jiǎn)單剖析下案例代碼逸雹,RetryTemplate
承擔(dān)了重試執(zhí)行者的角色营搅,它可以設(shè)置SimpleRetryPolicy
(重試策略,設(shè)置重試上限梆砸,重試的根源實(shí)體)转质,FixedBackOffPolicy
(固定的回退策略,設(shè)置執(zhí)行重試回退的時(shí)間間隔)帖世。
RetryTemplate
通過execute
提交執(zhí)行操作休蟹,需要準(zhǔn)備RetryCallback
和RecoveryCallback
兩個(gè)類實(shí)例
-
RetryCallback
:對(duì)應(yīng)的就是重試回調(diào)邏輯實(shí)例,包裝正常的功能操作 -
RecoveryCallback
:實(shí)現(xiàn)的是整個(gè)執(zhí)行操作結(jié)束的恢復(fù)操作實(shí)例
注意
:只有在調(diào)用的時(shí)候拋出了異常日矫,并且異常是在exceptionMap
中配置的異常赂弓,才會(huì)執(zhí)行重試操作,否則就調(diào)用到excute
方法的第二個(gè)執(zhí)行方法RecoveryCallback
中
當(dāng)然哪轿,重試策略還有很多種盈魁,回退策略也是:
- 重試策略
-
NeverRetryPolicy
: 只允許調(diào)用RetryCallback
一次,不允許重試 -
AlwaysRetryPolicy
: 允許無限重試窃诉,直到成功杨耙,此方式邏輯不當(dāng)會(huì)導(dǎo)致死循環(huán) -
SimpleRetryPolicy
: 固定次數(shù)重試策略,默認(rèn)重試最大次數(shù)為3次飘痛,RetryTemplate
默認(rèn)使用的策略 -
TimeoutRetryPolicy
: 超時(shí)時(shí)間重試策略按脚,默認(rèn)超時(shí)時(shí)間為1秒,在指定的超時(shí)時(shí)間內(nèi)允許重試 -
ExceptionClassifierRetryPolicy
: 設(shè)置不同異常的重試策略敦冬,類似組合重試策略辅搬,區(qū)別在于這里只區(qū)分不同異常的重試 -
CircuitBreakerRetryPolicy
: 有熔斷功能的重試策略,需設(shè)置3個(gè)參數(shù)openTimeout
脖旱、resetTimeout
和delegate
-
CompositeRetryPolicy
: 組合重試策略堪遂,有兩種組合方式,樂觀組合重試策略
是指只要有一個(gè)策略允許即可以重試萌庆,悲觀組合重試策略
是指只要有一個(gè)策略不允許即可以重試溶褪,但不管哪種組合方式,組合中的每一個(gè)策略都會(huì)執(zhí)行
-
- 重試回退策略
重試回退策略践险,指的是每次重試是立即重試還是等待一段時(shí)間后重試猿妈。
默認(rèn)情況下是立即重試
,如果需要配置等待一段時(shí)間后重試則需要指定回退策略BackoffRetryPolicy
巍虫。-
NoBackOffPolicy
: 無退避算法策略彭则,每次重試時(shí)立即重試 -
FixedBackOffPolicy
: 固定時(shí)間的退避策略,需設(shè)置參數(shù)sleeper
和backOffPeriod
占遥,sleeper
指定等待策略俯抖,默認(rèn)是Thread.sleep
,即線程休眠瓦胎,backOffPeriod
指定休眠時(shí)間芬萍,默認(rèn)1秒 -
UniformRandomBackOffPolicy
: 隨機(jī)時(shí)間退避策略尤揣,需設(shè)置sleeper
、minBackOffPeriod
和maxBackOffPeriod
柬祠,該策略在minBackOffPeriod,maxBackOffPeriod
之間取一個(gè)隨機(jī)休眠時(shí)間北戏,minBackOffPeriod
默認(rèn)500毫秒,maxBackOffPeriod
默認(rèn)1500毫秒 -
ExponentialBackOffPolicy
: 指數(shù)退避策略漫蛔,需設(shè)置參數(shù)sleeper
最欠、initialInterval
、maxInterval
和multiplier
惩猫,initialInterval
指定初始休眠時(shí)間,默認(rèn)100毫秒蚜点,maxInterval
指定最大休眠時(shí)間轧房,默認(rèn)30秒,multiplier
指定乘數(shù)绍绘,即下一次休眠時(shí)間為當(dāng)前休眠時(shí)間*multiplier -
ExponentialRandomBackOffPolicy
: 隨機(jī)指數(shù)退避策略奶镶,引入隨機(jī)乘數(shù)可以實(shí)現(xiàn)隨機(jī)乘數(shù)回退
-
上面的代碼的話,簡(jiǎn)單的設(shè)置了重試間隔為1秒陪拘,重試的異常是RemoteAccessException
厂镇,下面就是測(cè)試代碼的情況: 重試第二次成功的情況:
重試一次以后,遇到了沒有指出需要重試的異常左刽,直接結(jié)束重試捺信,調(diào)用retryContext
重試了三次后,達(dá)到了最大重試次數(shù)欠痴,調(diào)用retryContext
1.3 注解使用方式
既然是Spring
家族的東西迄靠,那么自然就支持和Spring-Boot
整合
1.3.1 注解介紹
我們只要在需要重試的方法上加@Retryable
,在重試失敗的回調(diào)方法上加@Recover
喇辽,下面是這些注解的屬性
@EnableRetry
掌挚,表示是否開啟重試
序號(hào) | 屬性 | 類型 | 默認(rèn)值 | 說明 |
---|---|---|---|---|
1 | proxyTargetClass | boolean | false | 指示是否要?jiǎng)?chuàng)建基于子類的(CGLIB)代理,而不是創(chuàng)建標(biāo)準(zhǔn)的基于Java接口的代理 |
@Retryable
菩咨,標(biāo)注此注解的方法在發(fā)生異常時(shí)會(huì)進(jìn)行重試
序號(hào) | 屬性 | 類型 | 默認(rèn)值 | 說明 |
---|---|---|---|---|
1 | interceptor | String | "" | 將interceptor的bean名稱應(yīng)用到retryable() |
2 | value | Class[] | {} | 可重試的異常類型 |
3 | label | String | "" | 統(tǒng)計(jì)報(bào)告的唯一標(biāo)簽吠式。如果沒有提供,調(diào)用這可以選擇忽略它抽米,或者提供默認(rèn)值 |
4 | maxAttempts | int | 3 | 嘗試的最大次數(shù)(包括第一次失敗)特占,默認(rèn)為3次 |
5 | backoff | @Backoff | @Backoff() | 指定用于重試此操作的backoff屬性。默認(rèn)為空 |
6 | exclude | Class[] | {} | 排除可重試的異常類型 |
@Backoff
序號(hào) | 屬性 | 類型 | 默認(rèn)值 | 說明 |
---|---|---|---|---|
1 | delay | long | 0 | 如果不設(shè)置則默認(rèn)使用 1000 milliseconds 重試等待 |
2 | maxDelay | long | 0 | 最大重試等待時(shí)間 |
3 | multiplier | long | 0 | 用于計(jì)算下一個(gè)延遲的乘數(shù)(大于0生效) |
4 | random | boolean | false | 隨機(jī)重試等待時(shí)間 |
1.3.2 pom.xml
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>1.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.1</version>
</dependency>
1.3.3 代碼
在application
啟動(dòng)類上加上@EnableRetry
的注解
@EnableRetry
public class Application {
...
}
為了方便測(cè)試云茸,這里寫了一個(gè)SpringBootTest
的測(cè)試基類摩钙,需要使用SpringBootTest
的只要繼承這個(gè)類就好了
package com.zgd.demo.thread.test;
/**
* @Author: zgd
* @Description:
*/
import com.zgd.demo.thread.Application;
import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
/**
* @Author: zgd
* @Date: 18/09/29 20:33
* @Description:
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
@Slf4j
public class MyBaseTest {
@Before
public void init() {
log.info("----------------測(cè)試開始---------------");
}
@After
public void after() {
log.info("----------------測(cè)試結(jié)束---------------");
}
}
建一個(gè)service類
package com.zgd.demo.thread.retry.spring;
import com.zgd.demo.thread.retry.RetryDemoTask;
import com.zgd.demo.thread.test.MyBaseTest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.remoting.RemoteAccessException;
import org.springframework.retry.ExhaustedRetryException;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Component;
/**
* @Author: zgd
* @Description:
*/
@Service
@Slf4j
public class SpringRetryDemo {
/**
* 重試所調(diào)用方法
* @param param
* @return
*/
@Retryable(value = {RemoteAccessException.class},maxAttempts = 3,backoff = @Backoff(delay = 2000L,multiplier = 2))
public boolean call(String param){
return RetryDemoTask.retryTask(param);
}
/**
* 達(dá)到最大重試次數(shù),或拋出了一個(gè)沒有指定進(jìn)行重試的異常
* recover 機(jī)制
* @param e 異常
*/
@Recover
public boolean recover(Exception e,String param) {
log.error("達(dá)到最大重試次數(shù),或拋出了一個(gè)沒有指定進(jìn)行重試的異常:",e);
return false;
}
}
然后我們調(diào)用這個(gè)service里面的call方法
package com.zgd.demo.thread.retry.spring;
import com.zgd.demo.thread.test.MyBaseTest;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @Author: zgd
* @Description:
*/
@Component
@Slf4j
public class SpringRetryDemoTest extends MyBaseTest {
@Autowired
private SpringRetryDemo springRetryDemo;
@Test
public void retry(){
boolean abc = springRetryDemo.call("abc");
log.info("--結(jié)果是:{}--",abc);
}
}
這里我依然是RemoteAccessException
的異常才重試,@Backoff(delay = 2000L,multiplier = 2))
表示第一次間隔2秒查辩,以后都是次數(shù)的2倍胖笛,也就是第二次4秒网持,第三次6秒.
1.3.4 測(cè)試結(jié)果
遇到了沒有指定重試的異常,這里指定重試的異常是 @Retryable(value = {RemoteAccessException.class}...
,所以拋出參數(shù)異常IllegalArgumentException
的時(shí)候长踊,直接回調(diào)@Recover
的方法
重試達(dá)到最大重試次數(shù)時(shí)功舀,調(diào)用@Recover
的方法
重試到最后一次沒有報(bào)錯(cuò),返回false
1.4 @Retryable和@Recover注解使用問題
@Retryable
和@Recover
是Spring Framework
中的注解身弊,用于支持在方法執(zhí)行期間發(fā)生異常時(shí)的重試和恢復(fù)操作辟汰。
-
@Retryable
:注解用于標(biāo)記方法,在方法執(zhí)行期間發(fā)生異常時(shí)進(jìn)行重試阱佛。重試行為可以使用Spring Retry
框架提供的默認(rèn)策略或自定義策略來定義帖汞。可以指定要重試的異常類型以及最大重試次數(shù)和重試間隔等參數(shù)凑术。 -
@Recover
:注解用于標(biāo)記一個(gè)恢復(fù)方法翩蘸,在最終重試失敗后執(zhí)行該方法』囱罚恢復(fù)方法應(yīng)具有與原始方法相同的參數(shù)和返回類型
催首,并且應(yīng)在同一類中聲明
。如果未指定恢復(fù)方法泄鹏,則重試失敗后將拋出最后一次異常郎任。
1.4.1 @Retryable和@Recover必須定義在一個(gè)類當(dāng)中嗎
@Retryable
和@Recover
注解必須定義在同一個(gè)類中。這是因?yàn)?code>@Retryable注解標(biāo)記的方法需要調(diào)用重試邏輯备籽,而@Recover
注解標(biāo)記的方法需要提供重試失敗后的恢復(fù)邏輯舶治。因此,這兩個(gè)方法必須在同一個(gè)類中车猬,以便它們能夠相互調(diào)用歼疮。
例如,以下示例演示了@Retryable
和@Recover
注解在同一個(gè)類中的使用
@Service
public class MyService {
@Retryable(RuntimeException.class)
public void myMethod() {
// Some code that may throw a RuntimeException
}
@Recover
public void recover() {
// Recovery logic goes here
}
}
在上面的示例中诈唬,MyService
類中的myMethod()
方法使用@Retryable
注解進(jìn)行標(biāo)記韩脏,以便在發(fā)生RuntimeException
異常時(shí)進(jìn)行重試。如果重試最終失敗铸磅,則recover()
方法將被調(diào)用以提供恢復(fù)邏輯赡矢。由于這兩個(gè)方法在同一個(gè)類中定義,因此它們可以相互調(diào)用阅仔。
1.4.2 如果一個(gè)類中有多個(gè)@Recover和@Retryable怎么區(qū)分
如果一個(gè)類中有多個(gè)方法標(biāo)記了@Retryable
和@Recover
注解吹散,你可以通過value
屬性來區(qū)分它們。value屬性允許你指定一個(gè)異常類型的數(shù)組八酒,以區(qū)分在方法執(zhí)行期間拋出的不同異常類型空民。
例如,以下示例演示了在同一個(gè)類中有多個(gè)使用@Retryable和@Recover
注解的方法的情況:
@Service
public class MyService {
@Retryable(value = {IOException.class})
public void methodA() throws IOException {
// Some code that may throw an IOException
}
@Recover
public void recoverA(IOException e) {
// Recovery logic for methodA() goes here
}
@Retryable(value = {NullPointerException.class})
public void methodB() throws NullPointerException {
// Some code that may throw a NullPointerException
}
@Recover
public void recoverB(NullPointerException e) {
// Recovery logic for methodB() goes here
}
}
在上面的示例中,methodA()和methodB()
方法都標(biāo)記為@Retryable
注解界轩,以便在拋出IOException或NullPointerException
異常時(shí)進(jìn)行重試画饥。然后,對(duì)于每個(gè)方法浊猾,都定義了一個(gè)恢復(fù)方法recoverA()和recoverB()
抖甘,分別提供特定于該方法的恢復(fù)邏輯。由于每個(gè)方法都在@Retryable
注解的value
屬性中指定了不同的異常類型葫慎,因此Spring
框架可以區(qū)分它們衔彻,并在適當(dāng)?shù)臅r(shí)候調(diào)用相應(yīng)的方法。
1.4.3 如果異常也一樣呢
如果多個(gè)方法在拋出相同的異常
時(shí)都需要進(jìn)行重試和恢復(fù)操作偷办,你可以在每個(gè)方法上使用相同的@Retryable和@Recover
注解艰额。在這種情況下,Spring
框架會(huì)自動(dòng)根據(jù)需要調(diào)用相應(yīng)的恢復(fù)方法椒涯。
例如柄沮,以下示例演示了在同一個(gè)類中有多個(gè)方法拋出相同異常,并且都需要重試和恢復(fù)的情況:
@Service
public class MyService {
@Retryable(RuntimeException.class)
public void methodA() {
// Some code that may throw a RuntimeException
}
@Retryable(RuntimeException.class)
public void methodB() {
// Some code that may throw a RuntimeException
}
@Recover
public void recover() {
// Recovery logic goes here
}
}
在上面的示例中逐工,methodA()和methodB()
方法都標(biāo)記為@Retryable
注解,以便在拋出RuntimeException
異常時(shí)進(jìn)行重試漂辐。由于它們都使用相同的異常類型和重試策略泪喊,因此它們也可以使用相同的恢復(fù)方法recover()
。在最終重試失敗時(shí)髓涯,Spring
框架將調(diào)用recover()
方法以提供恢復(fù)邏輯袒啼。
1.4.4 那怎么在recover中知道是哪個(gè)方法發(fā)生了異常呢
在@Recover
注解標(biāo)記的恢復(fù)方法中,可以通過方法的參數(shù)
獲取拋出異常的方法和異常信息纬纪。具體來說蚓再,@Recover
方法可以接受與@Retryable
注解標(biāo)記的方法相同的參數(shù),以便在恢復(fù)操作中訪問異常信息和方法參數(shù)包各。
例如摘仅,以下示例演示了在@Retryable
方法和@Recover
方法中訪問異常和方法參數(shù)的方式:
@Service
public class MyService {
@Retryable(RuntimeException.class)
public void methodA(String arg) {
// Some code that may throw a RuntimeException
}
@Retryable(RuntimeException.class)
public void methodB(int arg) {
// Some code that may throw a RuntimeException
}
@Recover
public void recover(Exception e, Object... args) {
if (args[0] instanceof String) {
// Recovery logic for methodA() goes here
} else if (args[0] instanceof Integer) {
// Recovery logic for methodB() goes here
}
}
}
在上面的示例中,methodA()和methodB()
方法都標(biāo)記為@Retryable
注解问畅,以便在拋出RuntimeException
異常時(shí)進(jìn)行重試娃属。在@Recover
注解標(biāo)記的恢復(fù)方法中,可以通過方法的參數(shù)訪問拋出異常的方法和方法參數(shù)护姆。在上述示例中矾端,我們檢查第一個(gè)參數(shù)的類型以確定是哪個(gè)方法拋出了異常,從而提供相應(yīng)的恢復(fù)邏輯卵皂。
需要注意的是秩铆,@Retryable
方法和@Recover
方法中的參數(shù)類型和數(shù)量必須相同,否則Spring
框架將無法確定哪個(gè)方法拋出了異常灯变。
2 重試之Guava-Retry
2.1 簡(jiǎn)介
Guava retryer
工具與spring-retry
類似殴玛,都是通過定義重試者角色來包裝正常邏輯重試捅膘,但是Guava retryer
有更優(yōu)的策略定義,在支持重試次數(shù)和重試頻度控制基礎(chǔ)上族阅,能夠兼容支持多個(gè)異陈耍或者自定義實(shí)體對(duì)象的重試源定義,讓重試功能有更多的靈活性坦刀。
Guava Retryer
也是線程安全的愧沟,入口調(diào)用邏輯采用的是Java.util.concurrent.Callable
的call
方法,示例代碼如下:
2.2 pom.xml
pom.xml加入依賴
<!-- https://mvnrepository.com/artifact/com.github.rholder/guava-retrying -->
<dependency>
<groupId>com.github.rholder</groupId>
<artifactId>guava-retrying</artifactId>
<version>2.0.0</version>
</dependency>
2.3 代碼操作
更改一下測(cè)試的任務(wù)方法
package com.zgd.demo.thread.retry;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomUtils;
import org.springframework.remoting.RemoteAccessException;
/**
* @Author: zgd
* @Description:
*/
@Slf4j
public class RetryDemoTask {
/**
* 重試方法
* @return
*/
public static boolean retryTask(String param) {
log.info("收到請(qǐng)求參數(shù):{}",param);
int i = RandomUtils.nextInt(0,11);
log.info("隨機(jī)生成的數(shù):{}",i);
if (i < 2) {
log.info("為0,拋出參數(shù)異常.");
throw new IllegalArgumentException("參數(shù)異常");
}else if (i < 5){
log.info("為1,返回true.");
return true;
}else if (i < 7){
log.info("為2,返回false.");
return false;
}else{
//為其他
log.info("大于2,拋出自定義異常.");
throw new RemoteAccessException("大于2,拋出自定義異常");
}
}
}
這里設(shè)定跟Spring-Retry
不一樣鲤遥,我們可以根據(jù)返回的結(jié)果來判斷是否重試沐寺,比如返回false我們就重試
package com.zgd.demo.thread.retry.guava;
import com.github.rholder.retry.*;
import com.zgd.demo.thread.retry.RetryDemoTask;
import org.junit.Test;
import org.springframework.remoting.RemoteAccessException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
/**
* @Author: zgd
* @Description:
*/
public class GuavaRetryTest {
@Test
public void fun01(){
// RetryerBuilder 構(gòu)建重試實(shí)例 retryer,可以設(shè)置重試源且可以支持多個(gè)重試源,可以配置重試次數(shù)或重試超時(shí)時(shí)間盖奈,以及可以配置等待時(shí)間間隔
Retryer<Boolean> retryer = RetryerBuilder.<Boolean> newBuilder()
.retryIfExceptionOfType(RemoteAccessException.class)//設(shè)置異常重試源
.retryIfResult(res-> res==false) //設(shè)置根據(jù)結(jié)果重試
.withWaitStrategy(WaitStrategies.fixedWait(3, TimeUnit.SECONDS)) //設(shè)置等待間隔時(shí)間
.withStopStrategy(StopStrategies.stopAfterAttempt(3)) //設(shè)置最大重試次數(shù)
.build();
try {
retryer.call(() -> RetryDemoTask.retryTask("abc"));
} catch (Exception e) {
e.printStackTrace();
}
}
}
2.4 運(yùn)行結(jié)果
遇到了我們指定的需要重試的異常混坞,進(jìn)行重試,間隔是3秒
重試次數(shù)超過了最大重試次數(shù)
返回為true钢坦,直接結(jié)束重試
遇到了沒有指定重試的異常究孕,結(jié)束重試
返回false,重試
2.5 Guava配置策略
我們可以更靈活的配置重試策略爹凹,比如:
-
retryIfException
:retryIfException
厨诸,拋出runtime
異常、checked
異常時(shí)都會(huì)重試禾酱,但是拋出error
不會(huì)重試微酬。 -
retryIfRuntimeException
:retryIfRuntimeException
只會(huì)在拋runtime
異常的時(shí)候才重試,checked 異常和error 都不重試颤陶。 -
retryIfExceptionOfType
:retryIfExceptionOfType
允許我們只在發(fā)生特定異常的時(shí)候才重試颗管,比如NullPointerException
和IllegalStateException
都屬于 runtime 異常,也包括自定義的error滓走。
如:
retryIfExceptionOfType(NullPointerException.class)
垦江,只在拋出空指針異常重試 -
retryIfResult
:retryIfResult
可以指定Callable
方法在返回值的時(shí)候進(jìn)行重試,如- 返回
false
重試
.retryIfResult(Predicates.equalTo(false))
- 以
_error
結(jié)尾才重試
.retryIfResult(Predicates.containsPattern("_error$"))
- 返回為空時(shí)重試
.retryIfResult(res-> res==null)
- 返回
-
RetryListener
: 當(dāng)發(fā)生重試之后搅方,假如我們需要做一些額外的處理動(dòng)作疫粥,比如log
一下異常,那么可以使用RetryListener
腰懂。每次重試之后梗逮,guava-retrying
會(huì)自動(dòng)回調(diào)我們注冊(cè)的監(jiān)聽⌒辶铮可以注冊(cè)多個(gè)RetryListener
慷彤,會(huì)按照注冊(cè)順序依次調(diào)用。
.withRetryListener(new RetryListener {
@Override
public <T> void onRetry(Attempt<T> attempt) {
logger.error("第【{}】次調(diào)用失敗" , attempt.getAttemptNumber());
}
})
2.6 主要接口
序號(hào) | 接口 | 描述 | 備注 |
---|---|---|---|
1 | Attempt | 一次執(zhí)行任務(wù) | |
2 | AttemptTimeLimiter | 單次任務(wù)執(zhí)行時(shí)間限制 | 如果單詞任務(wù)執(zhí)行超時(shí),則終止執(zhí)行當(dāng)前任務(wù) |
3 | BlockStrategies | 任務(wù)阻塞策略 | 通俗的講就是當(dāng)前任務(wù)執(zhí)行完底哗,下次任務(wù)還沒開始這段時(shí)間做什么岁诉,默認(rèn)策略為:BlockStrategies.THREAD_SLEEP_STRATEGY
|
4 | RetryException | 重試異常 | |
5 | RetryListener | 自定義重試監(jiān)聽器 | 可以用于異步記錄錯(cuò)誤日志 |
6 | StopStrategy | 停止重試策略 | |
7 | WaitStrategy | 等待時(shí)長(zhǎng)策略 | (控制時(shí)間間隔),返回結(jié)果為下次執(zhí)行時(shí)長(zhǎng) |
StopStrategy
跋选,停止重試策略涕癣,提供三種
-
StopAfterDelayStrategy
設(shè)定一個(gè)最長(zhǎng)允許的執(zhí)行時(shí)間,比如設(shè)定最長(zhǎng)執(zhí)行10S前标,無論任務(wù)執(zhí)行次數(shù)坠韩,只要重試的時(shí)候超出了最長(zhǎng)時(shí)間最铁,則任務(wù)終止鞭达,并返回重試異常RetryException
-
NeverStopStrategy
不停止倔叼,用于需要一直輪訓(xùn)知道返回期望結(jié)果的情況: -
StopAfterAttemptStrategy
設(shè)定最大重試次數(shù)厌漂,如果超出最大重試次數(shù)則停止重試,并返回重試異常:
WaitStrategy
低散,等待時(shí)長(zhǎng)策略
-
FixedWaitStrategy
固定等待時(shí)長(zhǎng)策略羽历, -
RandomWaitStrategy
隨機(jī)等待時(shí)長(zhǎng)策略 (可以提供一個(gè)最小和最大時(shí)長(zhǎng)座舍,等待時(shí)長(zhǎng)為其區(qū)間隨機(jī)值) -
IncrementingWaitstrategy
遞增等待時(shí)長(zhǎng)策略 (提供一個(gè)初始值和步長(zhǎng)稽犁,等待時(shí)間隨重試次數(shù)增加而增加) -
ExponentialWaitstrategy
指數(shù)等待時(shí)長(zhǎng)策略; -
FibonacciWaitStrategy
Fibonacci
等待時(shí)長(zhǎng)策略焰望, -
ExceptionWaitStrategy
異常時(shí)長(zhǎng)等待策略; -
CompositeWaitStrategy
復(fù)合時(shí)長(zhǎng)等待策略;
3 使用斷路器Hystrix實(shí)現(xiàn)熔斷機(jī)制
除了重試機(jī)制外,熔斷機(jī)制也是一種常見的容錯(cuò)處理手段已亥。Hystrix
是一款流行的斷路器實(shí)現(xiàn)庫(kù)熊赖,可以與Spring Boot
集成,用于實(shí)現(xiàn)熔斷機(jī)制陷猫。
3.1 添加依賴
在pom.xml中添加Hystrix的依賴:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
3.2 配置啟用Hystrix
在Spring Boot
的主類上添加@EnableHystrix
注解:
@SpringBootApplication
@EnableHystrix
public class YourApplication {
public static void main(String[] args) {
SpringApplication.run(YourApplication.class, args);
}
}
3.3 使用Hystrix實(shí)現(xiàn)熔斷
3.3.1 代碼示例
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
@Service
public class ThirdPartyService {
@HystrixCommand(fallbackMethod = "fallback")
public String callThirdPartyApi() {
// 調(diào)用第三方API的邏輯
// ...
}
public String fallback() {
// 熔斷時(shí)的降級(jí)邏輯
// ...
}
}
在上述示例中秫舌,通過@HystrixCommand
注解標(biāo)記了callThirdPartyApi
方法的妖,指定了熔斷時(shí)執(zhí)行的降級(jí)方法fallback
文章參考:
https://mp.weixin.qq.com/s/nUD5kT2BcEEbmfeyG7rnDQ
https://mp.weixin.qq.com/s/pEPHzbPdmCsNeFeF2LwlXg
4 Fast-Retry
4.1 前言
假設(shè)系統(tǒng)里有100萬個(gè)用戶绣檬,然后要輪詢重試的獲取每個(gè)用戶的身份信息, 如果還在使用SpringRetry和GuavaRetry 之類的這種單任務(wù)的同步重試框架,那可能到猴年馬月也處理不完嫂粟, 即使加再多的機(jī)器和線程也是杯水車薪娇未, 而Fast-Retry正是為這種場(chǎng)景而生
4.1.1 簡(jiǎn)介
fast-retry是一個(gè)高性能的多任務(wù)重試框架,支持百萬級(jí)任務(wù)的異步重試星虹、以及支持編程式和注解聲明式等多種使用方式零抬、 也支持自定義結(jié)果重試邏輯。
Github 項(xiàng)目地址:https://github.com/burukeYou/fast-retry
與主流的Spring-Retry
, Guava-Retry
等單任務(wù)同步重試框架不同宽涌,Fast-Retry
是一個(gè)支持異步重試框架平夜,支持異步任務(wù)的重試、超時(shí)等待卸亮、回調(diào)忽妒。Spring-Retry
, Guava-Retry
均無法支持大批量任務(wù)的重試,即使加入線程池也無法解決,因?yàn)閷?shí)際每個(gè)重試任務(wù)都是單獨(dú)的同步邏輯段直,然后會(huì)會(huì)占用過多線程資源導(dǎo)致大量任務(wù)在等待處理吃溅,隨著任務(wù)數(shù)的增加,系統(tǒng)吞吐量大大降低鸯檬,性能指數(shù)級(jí)降低决侈,而Fast-Retry
在異步重試下的性能是前者的指數(shù)倍。
即使拋開性能不談喧务, SpringRetry
使用繁瑣赖歌,不支持根據(jù)結(jié)果的進(jìn)行重試,GuavaRetry
雖然支持蹂楣,但是又沒有提供注解聲明式的使用俏站。
4.1.2 fast-retry相比其它重試框架快在哪里
與其說快在哪,不如說同步型重試框架慢在哪痊土。因?yàn)橥街卦囀?code>阻塞的同步的肄扎,比如有 100 個(gè)重試任務(wù),每個(gè)重試任務(wù)要重試 1 分鐘赁酝,線程池有 10 個(gè)線程池犯祠,最多只能同時(shí)處理10 個(gè)重試任務(wù)。
也就是說起碼要 10 分鐘后才去執(zhí)行剩下的 90 個(gè)任務(wù)酌呆。最后 100 個(gè)任務(wù)輪詢完都要 100 分鐘了衡载。實(shí)際沒必要等它輪詢完一個(gè)任務(wù)才去執(zhí)行下一個(gè)任務(wù),它開始輪詢一個(gè)任務(wù)后就可以開始去執(zhí)行下一個(gè)任務(wù)了隙袁。
具體來說痰娱,Fast-Retry
的快
主要體現(xiàn)在以下幾個(gè)方面:
- 異步執(zhí)行:Fast-Retry 通常采用異步方式執(zhí)行重試邏輯,這意味著它可以在等待重試間隔時(shí)不阻塞主線程菩收,從而提高應(yīng)用程序的整體響應(yīng)性和吞吐量梨睁。
- 非阻塞 I/O:在處理需要 I/O 操作(如網(wǎng)絡(luò)請(qǐng)求、文件讀寫等)的重試時(shí)娜饵,F(xiàn)ast-Retry 可以利用非阻塞 I/O 機(jī)制坡贺,這樣在等待 I/O 操作完成時(shí)不會(huì)占用寶貴的線程資源。
- 優(yōu)化的重試策略:
Fast-Retry
允許用戶自定義重試策略箱舞,包括重試次數(shù)遍坟、重試間隔、退避算法等晴股。通過智能的退避算法(如指數(shù)退避)愿伴,它可以在保持高效率的同時(shí)減少對(duì)資源的不必要消耗。 - 資源利用:Fast-Retry 框架可能會(huì)優(yōu)化資源的使用电湘,例如通過復(fù)用連接或線程來減少創(chuàng)建和銷毀資源的開銷隔节。
- 錯(cuò)誤處理:Fast-Retry 能夠快速識(shí)別和處理重試中的錯(cuò)誤万搔,減少錯(cuò)誤處理的時(shí)間開銷。
- 集成和擴(kuò)展性:Fast-Retry 框架往往設(shè)計(jì)得易于集成和擴(kuò)展官帘,這意味著它可以快速地被添加到現(xiàn)有的系統(tǒng)中瞬雹,并且可以根據(jù)需要進(jìn)行定制。
- 避免不必要的重試:Fast-Retry 能夠根據(jù)錯(cuò)誤類型或其他條件判斷是否需要重試刽虹,避免在明顯無望的情況下進(jìn)行無效的重試嘗試酗捌。
- 性能監(jiān)控:Fast-Retry 可能包含性能監(jiān)控功能,這有助于及時(shí)發(fā)現(xiàn)性能瓶頸并進(jìn)行優(yōu)化涌哲。
注意胖缤,F(xiàn)ast-Retry 的具體實(shí)現(xiàn)可能會(huì)根據(jù)不同的編程語言和框架有所不同。
4.2 使用例子
4.2.1 pom.xml
<dependency>
<groupId>io.github.burukeyou</groupId>
<artifactId>fast-retry-all</artifactId>
<version>0.2.0</version>
</dependency>
有以下三種方式去構(gòu)建我們的重試任務(wù)
4.2.2 使用重試隊(duì)列
RetryTask
就是可以配置我們重試任務(wù)的一些邏輯阀圾,比如怎么重試哪廓,怎么獲取重試結(jié)果,隔多久后重試初烘,在什么情況下重試涡真。它可以幫助我們更加自由的去構(gòu)建重試任務(wù)的邏輯。但如果只是簡(jiǎn)單使用肾筐,強(qiáng)烈建議使用FastRetryBuilder
或者 @FastRetry
注解
RetryQueue
就是一個(gè)執(zhí)行和調(diào)度我們重試任務(wù)的核心角色哆料,其在使用上與線程池的API方法基本一致
ExecutorService executorService = Executors.newFixedThreadPool(8);
RetryQueue queue = new FastRetryQueue(executorService);
RetryTask<String> task = new RetryTask<String>() {
int result = 0 ;
// 下一次重試的間隔
@Override
public long waitRetryTime() {
return 2000;
}
// 執(zhí)行重試,每次重試回調(diào)此方法
@Override
public boolean retry() {
return ++result < 5;
}
// 獲取重試結(jié)果
@Override
public String getResult() {
return result + "";
}
};
CompletableFuture<String> future = queue.submit(task);
log.info("任務(wù)結(jié)束 結(jié)果:{}",future.get());
4.2.3 使用FastRetryBuilder
底層還是使用的RetryQueue
去處理吗铐, 只是幫我們簡(jiǎn)化了構(gòu)建RetryTask的邏輯
RetryResultPolicy<String> resultPolicy = result -> result.equals("444");
FastRetryer<String> retryer = FastRetryBuilder.<String>builder()
.attemptMaxTimes(3)
.waitRetryTime(3, TimeUnit.SECONDS)
.retryIfException(true)
.retryIfExceptionOfType(TimeoutException.class)
.exceptionRecover(true)
.resultPolicy(resultPolicy)
.build();
CompletableFuture<String> future = retryer.submit(() -> {
log.info("重試");
//throw new Exception("test");
//int i = 1/0;
if (0 < 10){
throw new TimeoutException("test");
}
return "444";
});
String o = future.get();
log.info("結(jié)果{}", o);
4.2.4 使用@FastRetry注解
底層還是使用的RetryQueue
去處理东亦, 只是幫我們簡(jiǎn)化了構(gòu)建RetryTask
的邏輯,并且與Spring
進(jìn)行整合能對(duì)Spring
的bean
標(biāo)記了FastRetry
注解的方法進(jìn)行代理唬渗, 提供了重試任務(wù)注解聲明式的使用方式
- 依賴
Spring
環(huán)境典阵,所以需要在Spring
配置類加上@EnableFastRetry
注解啟用配置 , 這個(gè)@FastRetry
注解的使用才會(huì)生效 - 如果將結(jié)果類型使用
CompletableFuture
包裝镊逝,自動(dòng)進(jìn)行異步輪詢返回壮啊,否則同步阻塞等待重試結(jié)果(推薦)
下面定義等價(jià)于 RetryQueue.execute方法
// 如果發(fā)生異常,每隔兩秒重試一次
@FastRetry(retryWait = @RetryWait(delay = 2))
public String retryTask(){
return "success";
}
下面定義等價(jià)于 RetryQueue.submit方法,支持異步輪詢
@FastRetry(retryWait = @RetryWait(delay = 2))
public CompletableFuture<String> retryTask(){
return CompletableFuture.completedFuture("success");
}
4.2.5 自定義重試注解
如果不喜歡或者需要更加通用化的貼近業(yè)務(wù)的重試注解蹋半,提供一些默認(rèn)的參數(shù)和處理邏輯他巨,可以自行定義一個(gè)重試注解并標(biāo)記上@FastRetry
并指定factory
充坑,然后實(shí)現(xiàn)AnnotationRetryTaskFactory
接口實(shí)現(xiàn)自己的構(gòu)建重試任務(wù)的邏輯即可减江。@FastRetry
默認(rèn)實(shí)現(xiàn)就是:FastRetryAnnotationRetryTaskFactory
無論是使用以上哪種方式去構(gòu)建重試任務(wù),都建議使用異步重試的方法捻爷,即返回結(jié)果是CompletableFuture
的方法辈灼, 然后使用CompletableFuture
的whenComplete
方法去等待異步重試任務(wù)的執(zhí)行結(jié)果。
4.3 實(shí)際操作
假如有一個(gè)天氣服務(wù)的重試任務(wù)也榄,需要重試N次才可能獲取到某城市的天氣情況巡莹。分別使用Fast-Retry
注解和Spring-Retry
注解去并發(fā)獲取1000個(gè)城市的天氣情況司志,看下系統(tǒng)耗時(shí)。同樣的邏輯降宅,Spring-Retry需要1256秒左右骂远,F(xiàn)ast-Retry只需要10秒左右
// 天氣服務(wù)
@Component
public class WeatherService {
// Fast-Retry 重試獲取天氣城市天氣情況
@FastRetry(
maxAttempts = 100,
retryWait = @RetryWait(delay = 2,timeUnit = TimeUnit.SECONDS))
public CompletableFuture<WeatherResult> getFutureWeatherForCompare(String cityName){
log.info("WeatherService進(jìn)行重試 次數(shù):{} 城市: {}",++index,cityName);
WeatherResult weather = WeatherServer.getWeather(cityName);
if (weather == null){
//繼續(xù)重試
throw new RuntimeException("模擬異常進(jìn)行重試");
}
return FastRetryBuilder.of(weather);
}
// Spring-Retry 重試獲取天氣城市天氣情況
@Retryable(maxAttempts = 100,backoff = @Backoff(delay = 2000))
public WeatherResult getSpringWeatherForCompare(String cityName){
log.info("WeatherService進(jìn)行重試 次數(shù):{} 城市: {}",++index,cityName);
WeatherResult weather = WeatherServer.getWeather(cityName);
if (weather == null){
//繼續(xù)重試
throw new RuntimeException("模擬異常進(jìn)行重試");
}
return weather;
}
}
使用Spring-Retry去執(zhí)行1000個(gè)重試任務(wù)
/**
* spring-retry注解-測(cè)試
* @throws Exception
*/
@Test
public void testFastRetryManyTaskForSpring() throws Exception {
List<CompletableFuture<WeatherResult>> futures = new ArrayList<>();
ExecutorService pool = Executors.newFixedThreadPool(8);
StopWatch stopWatch = new StopWatch();
stopWatch.start();
int taskSize = 1000;
for (int i = 0; i < taskSize; i++) {
WeatherService taskWeatherService = context.getBean(WeatherService.class);
CompletableFuture<WeatherResult> testFuture = new CompletableFuture<>();
futures.add(testFuture);
String cityName = "北京" + i;
pool.execute(() -> {
WeatherResult weather = taskWeatherService.getSpringWeatherForCompare(cityName);
testFuture.complete(weather);
});
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
System.out.println("所有任務(wù)完成");
for (CompletableFuture<WeatherResult> future : futures) {
WeatherResult weatherResult = future.get();
log.info("城市輪詢結(jié)束 result:{}",weatherResult.data);
}
stopWatch.stop();
log.info("Spring-Retry測(cè)試總耗時(shí) 任務(wù)數(shù):{} 耗時(shí):{}",taskSize,stopWatch.getTotalTimeSeconds());
}
使用Fast-Retry去執(zhí)行1000個(gè)重試任務(wù)
/**
* 測(cè)試FastRetry注解測(cè)試
* @throws Exception
*/
@Test
public void testFastRetryManyTask() throws Exception {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
int taskSize = 1000;
List<CompletableFuture<WeatherResult>> futures = new ArrayList<>();
for (int i = 0; i < taskSize; i++) {
WeatherService taskWeatherService = context.getBean(WeatherService.class);
String cityName = "北京" + i;
CompletableFuture<WeatherResult> weather = taskWeatherService.getFutureWeatherForCompare(cityName);
futures.add(weather);
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
System.out.println("所有任務(wù)完成");
for (CompletableFuture<WeatherResult> future : futures) {
WeatherResult weatherResult = future.get();
log.info("城市輪詢結(jié)束 result:{}",weatherResult.data);
}
stopWatch.stop();
log.info("FastRetry測(cè)試總耗時(shí) 任務(wù)數(shù):{} 耗時(shí):{}",taskSize,stopWatch.getTotalTimeSeconds());
}