說明
最近公司在搞活動,需要依賴一個第三方接口仔掸,測試階段并沒有什么異常狀況脆贵,但上線后發(fā)現(xiàn)依賴的接口有時候會因為內(nèi)部錯誤而返回系統(tǒng)異常闪盔,雖然概率不大兽间,但總因為這個而報警總是不好的,何況死信隊列的消息還需要麻煩運維進行重新投遞残炮,所以加上重試機制勢在必行负懦。
重試機制可以保護系統(tǒng)減少因網(wǎng)絡(luò)波動筒捺、依賴服務(wù)短暫性不可用帶來的影響,讓系統(tǒng)能更穩(wěn)定的運行的一種保護機制纸厉。讓你原本就穩(wěn)如狗的系統(tǒng)更是穩(wěn)上加穩(wěn)系吭。
為了方便說明,先假設(shè)我們想要進行重試的方法如下:
@Slf4j
@Component
public class HelloService {
private static AtomicLong helloTimes = new AtomicLong();
public String hello(){
long times = helloTimes.incrementAndGet();
if (times % 4 != 0){
log.warn("發(fā)生異常颗品,time:{}", LocalTime.now() );
throw new HelloRetryException("發(fā)生Hello異常");
}
return "hello";
}
}
調(diào)用處:
@Slf4j
@Service
public class HelloRetryService implements IHelloService{
@Autowired
private HelloService helloService;
public String hello(){
return helloService.hello();
}
}
也就是說肯尺,這個接口每調(diào)4次才會成功一次。
手動重試
先來用最簡單的方法躯枢,直接在調(diào)用的時候進重試:
// 手動重試
public String hello(){
int maxRetryTimes = 4;
String s = "";
for (int retry = 1; retry <= maxRetryTimes; retry++) {
try {
s = helloService.hello();
log.info("helloService返回:{}", s);
return s;
} catch (HelloRetryException e) {
log.info("helloService.hello() 調(diào)用失敗则吟,準(zhǔn)備重試");
}
}
throw new HelloRetryException("重試次數(shù)耗盡");
}
輸出如下:
發(fā)生異常,time:10:17:21.079413300
helloService.hello() 調(diào)用失敗锄蹂,準(zhǔn)備重試
發(fā)生異常氓仲,time:10:17:21.085861800
helloService.hello() 調(diào)用失敗,準(zhǔn)備重試
發(fā)生異常败匹,time:10:17:21.085861800
helloService.hello() 調(diào)用失敗寨昙,準(zhǔn)備重試
helloService返回:hello
service.helloRetry():hello
程序在極短的時間內(nèi)進行了4次重試,然后成功返回掀亩。
這樣雖然看起來可以解決問題,但實踐上欢顷,由于沒有重試間隔槽棍,很可能當(dāng)時依賴的服務(wù)尚未從網(wǎng)絡(luò)異常中恢復(fù)過來,所以極有可能接下來的幾次調(diào)用都是失敗的抬驴。
而且炼七,這樣需要對代碼進行大量的侵入式修改,顯然布持,不優(yōu)雅豌拙。
代理模式
上面的處理方式由于需要對業(yè)務(wù)代碼進行大量修改,雖然實現(xiàn)了功能题暖,但是對原有代碼的侵入性太強按傅,可維護性差捉超。
所以需要使用一種更優(yōu)雅一點的方式,不直接修改業(yè)務(wù)代碼唯绍,那要怎么做呢拼岳?
其實很簡單,直接在業(yè)務(wù)代碼的外面再包一層就行了况芒,代理模式在這里就有用武之地了惜纸。
@Slf4j
public class HelloRetryProxyService implements IHelloService{
@Autowired
private HelloRetryService helloRetryService;
@Override
public String hello() {
int maxRetryTimes = 4;
String s = "";
for (int retry = 1; retry <= maxRetryTimes; retry++) {
try {
s = helloRetryService.hello();
log.info("helloRetryService 返回:{}", s);
return s;
} catch (HelloRetryException e) {
log.info("helloRetryService.hello() 調(diào)用失敗,準(zhǔn)備重試");
}
}
throw new HelloRetryException("重試次數(shù)耗盡");
}
}
這樣绝骚,重試邏輯就都由代理類來完成耐版,原業(yè)務(wù)類的邏輯就不需要修改了,以后想修改重試邏輯也只需要修改這個類就行了压汪,分工明確粪牲。比如,現(xiàn)在想要在重試之間加上一個延遲蛾魄,只需要做一點點修改即可:
@Override
public String hello() {
int maxRetryTimes = 4;
String s = "";
for (int retry = 1; retry <= maxRetryTimes; retry++) {
try {
s = helloRetryService.hello();
log.info("helloRetryService 返回:{}", s);
return s;
} catch (HelloRetryException e) {
log.info("helloRetryService.hello() 調(diào)用失敗虑瀑,準(zhǔn)備重試");
}
// 延時一秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
throw new HelloRetryException("重試次數(shù)耗盡");
}
代理模式雖然要更加優(yōu)雅,但是如果依賴的服務(wù)很多的時候滴须,要為每個服務(wù)都創(chuàng)建一個代理類舌狗,顯然過于麻煩,而且其實重試的邏輯都大同小異扔水,無非就是重試的次數(shù)和延時不一樣而已痛侍。如果每個類都寫這么一長串類似的代碼,顯然魔市,不優(yōu)雅主届!
JDK動態(tài)代理
這時候,動態(tài)代理就閃亮登場了待德。只需要寫一個代理處理類君丁,就可以開局一條狗,砍到九十九将宪。
@Slf4j
public class RetryInvocationHandler implements InvocationHandler {
private final Object subject;
public RetryInvocationHandler(Object subject) {
this.subject = subject;
}
@Override
public Object invoke(Object proxy, Method method, Object\[\] args) throws Throwable {
int times = 0;
while (times < RetryConstant.MAX_TIMES) {
try {
return method.invoke(subject, args);
} catch (Exception e) {
times++;
log.info("times:{},time:{}", times, LocalTime.now());
if (times >= RetryConstant.MAX_TIMES) {
throw new RuntimeException(e);
}
}
// 延時一秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return null;
}
/**
* 獲取動態(tài)代理
*
* @param realSubject 代理對象
* @return
*/
public static Object getProxy(Object realSubject) {
InvocationHandler handler = new RetryInvocationHandler(realSubject);
return Proxy.newProxyInstance(handler.getClass().getClassLoader(),
realSubject.getClass().getInterfaces(), handler);
}
}
來一發(fā)單元測:
@Test
public void helloDynamicProxy() {
IHelloService realService = new HelloService();
IHelloService proxyService = (IHelloService)RetryInvocationHandler.getProxy(realService);
String hello = proxyService.hello();
log.info("hello:{}", hello);
}
輸出如下:
hello times:1
發(fā)生異常绘闷,time:11:22:20.727586700
times:1,time:11:22:20.728083
hello times:2
發(fā)生異常,time:11:22:21.728858700
times:2,time:11:22:21.729343700
hello times:3
發(fā)生異常较坛,time:11:22:22.729706600
times:3,time:11:22:22.729706600
hello times:4
hello:hello
在重試了4次之后輸出了Hello
印蔗,符合預(yù)期。
動態(tài)代理可以將重試邏輯都放到一塊丑勤,顯然比直接使用代理類要方便很多华嘹,也更加優(yōu)雅。
不過不要高興的太早法竞,這里因為被代理的HelloService是一個簡單的類耙厚,沒有依賴其它類强挫,所以直接創(chuàng)建是沒有問題的,但如果被代理的類依賴了其它被Spring容器管理的類颜曾,則這種方式會拋出異常纠拔,因為沒有把被依賴的實例注入到創(chuàng)建的代理實例中。
這種情況下泛豪,就比較復(fù)雜了稠诲,需要從Spring容器中獲取已經(jīng)裝配好的,需要被代理的實例诡曙,然后為其創(chuàng)建代理類實例臀叙,并交給Spring容器來管理,這樣就不用每次都重新創(chuàng)建新的代理類實例了价卤。
話不多說劝萤,擼起袖子就是干。
新建一個工具類慎璧,用來獲取代理實例:
@Component
public class RetryProxyHandler {
@Autowired
private ConfigurableApplicationContext context;
public Object getProxy(Class clazz) {
// 1\. 從Bean中獲取對象
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory)context.getAutowireCapableBeanFactory();
Map<String, Object> beans = beanFactory.getBeansOfType(clazz);
Set<Map.Entry<String, Object>> entries = beans.entrySet();
if (entries.size() <= 0){
throw new ProxyBeanNotFoundException();
}
// 如果有多個候選bean, 判斷其中是否有代理bean
Object bean = null;
if (entries.size() > 1){
for (Map.Entry<String, Object> entry : entries) {
if (entry.getKey().contains(PROXY\_BEAN\_SUFFIX)){
bean = entry.getValue();
}
};
if (bean != null){
return bean;
}
throw new ProxyBeanNotSingleException();
}
Object source = beans.entrySet().iterator().next().getValue();
Object source = beans.entrySet().iterator().next().getValue();
// 2\. 判斷該對象的代理對象是否存在
String proxyBeanName = clazz.getSimpleName() + PROXY\_BEAN\_SUFFIX;
Boolean exist = beanFactory.containsBean(proxyBeanName);
if (exist) {
bean = beanFactory.getBean(proxyBeanName);
return bean;
}
// 3\. 不存在則生成代理對象
bean = RetryInvocationHandler.getProxy(source);
// 4\. 將bean注入spring容器
beanFactory.registerSingleton(proxyBeanName, bean);
return bean;
}
}
使用的是JDK動態(tài)代理:
@Slf4j
public class RetryInvocationHandler implements InvocationHandler {
private final Object subject;
public RetryInvocationHandler(Object subject) {
this.subject = subject;
}
@Override
public Object invoke(Object proxy, Method method, Object\[\] args) throws Throwable {
int times = 0;
while (times < RetryConstant.MAX_TIMES) {
try {
return method.invoke(subject, args);
} catch (Exception e) {
times++;
log.info("retry times:{},time:{}", times, LocalTime.now());
if (times >= RetryConstant.MAX_TIMES) {
throw new RuntimeException(e);
}
}
// 延時一秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return null;
}
/\*\*
\* 獲取動態(tài)代理
\*
\* @param realSubject 代理對象
*/
public static Object getProxy(Object realSubject) {
InvocationHandler handler = new RetryInvocationHandler(realSubject);
return Proxy.newProxyInstance(handler.getClass().getClassLoader(),
realSubject.getClass().getInterfaces(), handler);
}
}
至此床嫌,主要代碼就完成了,修改一下HelloService類胸私,增加一個依賴:
@Slf4j
@Component
public class HelloService implements IHelloService{
private static AtomicLong helloTimes = new AtomicLong();
@Autowired
private NameService nameService;
public String hello(){
long times = helloTimes.incrementAndGet();
log.info("hello times:{}", times);
if (times % 4 != 0){
log.warn("發(fā)生異常厌处,time:{}", LocalTime.now() );
throw new HelloRetryException("發(fā)生Hello異常");
}
return "hello " + nameService.getName();
}
}
NameService其實很簡單,創(chuàng)建的目的僅在于測試依賴注入的Bean能否正常運行岁疼。
@Service
public class NameService {
public String getName(){
return "Frank";
}
}
來一發(fā)測試:
@Test
public void helloJdkProxy() throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
IHelloService proxy = (IHelloService) retryProxyHandler.getProxy(HelloService.class);
String hello = proxy.hello();
log.info("hello:{}", hello);
}
hello times:1
發(fā)生異常阔涉,time:14:40:27.540672200
retry times:1,time:14:40:27.541167400
hello times:2
發(fā)生異常,time:14:40:28.541584600
retry times:2,time:14:40:28.542033500
hello times:3
發(fā)生異常捷绒,time:14:40:29.542161500
retry times:3,time:14:40:29.542161500
hello times:4
hello:hello Frank
完美瑰排,這樣就不用擔(dān)心依賴注入的問題了,因為從Spring容器中拿到的Bean對象都是已經(jīng)注入配置好的暖侨。當(dāng)然椭住,這里僅考慮了單例Bean的情況,可以考慮的更加完善一點字逗,判斷一下容器中Bean的類型是Singleton還是Prototype函荣,如果是Singleton則像上面這樣進行操作,如果是Prototype則每次都新建代理類對象扳肛。
另外,這里使用的是JDK動態(tài)代理乘碑,因此就存在一個天然的缺陷挖息,如果想要被代理的類,沒有實現(xiàn)任何接口兽肤,那么就無法為其創(chuàng)建代理對象套腹,這種方式就行不通了绪抛。
CGLib 動態(tài)代理
既然已經(jīng)說到了JDK動態(tài)代理,那就不得不提CGLib動態(tài)代理了电禀。使用JDK動態(tài)代理對被代理的類有要求幢码,不是所有的類都能被代理,而CGLib動態(tài)代理則剛好解決了這個問題尖飞。
創(chuàng)建一個CGLib動態(tài)代理類:
@Slf4j
public class CGLibRetryProxyHandler implements MethodInterceptor {
private Object target;//需要代理的目標(biāo)對象
//重寫攔截方法
@Override
public Object intercept(Object obj, Method method, Object\[\] arr, MethodProxy proxy) throws Throwable {
int times = 0;
while (times < RetryConstant.MAX_TIMES) {
try {
return method.invoke(target, arr);
} catch (Exception e) {
times++;
log.info("cglib retry :{},time:{}", times, LocalTime.now());
if (times >= RetryConstant.MAX_TIMES) {
throw new RuntimeException(e);
}
}
// 延時一秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
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;
}
}
想要換用CGLib動態(tài)代理症副,替換一下這兩行代碼即可:
// 3\. 不存在則生成代理對象
// bean = RetryInvocationHandler.getProxy(source);
CGLibRetryProxyHandler proxyHandler = new CGLibRetryProxyHandler();
bean = proxyHandler.getCglibProxy(source);
開始測試:
@Test
public void helloCGLibProxy() {
IHelloService proxy = (IHelloService) retryProxyHandler.getProxy(HelloService.class);
String hello = proxy.hello();
log.info("hello:{}", hello);
hello = proxy.hello();
log.info("hello:{}", hello);
}
hello times:1
發(fā)生異常,time:15:06:00.799679100
cglib retry :1,time:15:06:00.800175400
hello times:2
發(fā)生異常政基,time:15:06:01.800848600
cglib retry :2,time:15:06:01.801343100
hello times:3
發(fā)生異常贞铣,time:15:06:02.802180
cglib retry :3,time:15:06:02.802180
hello times:4
hello:hello Frank
hello times:5
發(fā)生異常,time:15:06:03.803933800
cglib retry :1,time:15:06:03.803933800
hello times:6
發(fā)生異常沮明,time:15:06:04.804945400
cglib retry :2,time:15:06:04.805442
hello times:7
發(fā)生異常辕坝,time:15:06:05.806886500
cglib retry :3,time:15:06:05.807881300
hello times:8
hello:hello Frank
這樣就很棒了,完美的解決了JDK動態(tài)代理帶來的缺陷荐健。優(yōu)雅指數(shù)上漲了不少酱畅。
但這個方案仍舊存在一個問題,那就是需要對原來的邏輯進行侵入式修改江场,在每個被代理實例被調(diào)用的地方都需要進行調(diào)整纺酸,這樣仍然會對原有代碼帶來較多修改。
Spring AOP
想要無侵入式的修改原有邏輯扛稽?想要一個注解就實現(xiàn)重試吁峻?用Spring AOP不就能完美實現(xiàn)嗎?使用AOP來為目標(biāo)調(diào)用設(shè)置切面在张,即可在目標(biāo)方法調(diào)用前后添加一些額外的邏輯用含。
先創(chuàng)建一個注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Retryable {
int retryTimes() default 3;
int retryInterval() default 1;
}
有兩個參數(shù),retryTimes 代表最大重試次數(shù)帮匾,retryInterval代表重試間隔啄骇。
然后在需要重試的方法上加上注解:
@Retryable(retryTimes = 4, retryInterval = 2)
public String hello(){
long times = helloTimes.incrementAndGet();
log.info("hello times:{}", times);
if (times % 4 != 0){
log.warn("發(fā)生異常,time:{}", LocalTime.now() );
throw new HelloRetryException("發(fā)生Hello異常");
}
return "hello " + nameService.getName();
}
接著瘟斜,進行最后一步缸夹,編寫AOP切面:
@Slf4j
@Aspect
@Component
public class RetryAspect {
@Pointcut("@annotation(com.mfrank.springboot.retry.demo.annotation.Retryable)")
private void retryMethodCall(){}
@Around("retryMethodCall()")
public Object retry(ProceedingJoinPoint joinPoint) throws InterruptedException {
// 獲取重試次數(shù)和重試間隔
Retryable retry = ((MethodSignature)joinPoint.getSignature()).getMethod().getAnnotation(Retryable.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("調(diào)用發(fā)生異常,開始重試螺句,retryTimes:{}", retryTimes);
}
Thread.sleep(retryInterval * 1000);
}
throw new RetryExhaustedException("重試次數(shù)耗盡", error);
}
}
開始測試:
@Autowired
private HelloService helloService;
@Test
public void helloAOP(){
String hello = helloService.hello();
log.info("hello:{}", hello);
}
輸出如下:
hello times:1
發(fā)生異常虽惭,time:16:49:30.224649800
調(diào)用發(fā)生異常,開始重試蛇尚,retryTimes:1
hello times:2
發(fā)生異常芽唇,time:16:49:32.225230800
調(diào)用發(fā)生異常,開始重試取劫,retryTimes:2
hello times:3
發(fā)生異常匆笤,time:16:49:34.225968900
調(diào)用發(fā)生異常研侣,開始重試,retryTimes:3
hello times:4
hello:hello Frank
這樣就相當(dāng)優(yōu)雅了炮捧,一個注解就能搞定重試庶诡,簡直不要更棒。
Spring 的重試注解
實際上Spring中就有比較完善的重試機制咆课,比上面的切面更加好用末誓,還不需要自己動手重新造輪子。
那讓我們先來看看這個輪子究竟好不好使傀蚌。
先引入重試所需的jar包:
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
然后在啟動類或者配置類上添加@EnableRetry注解基显,接下來在需要重試的方法上添加@Retryable注解(嗯?好像跟我自定義的注解一樣善炫?竟然抄襲我的注解撩幽! [手動滑稽] )
@Retryable
public String hello(){
long times = helloTimes.incrementAndGet();
log.info("hello times:{}", times);
if (times % 4 != 0){
log.warn("發(fā)生異常,time:{}", LocalTime.now() );
throw new HelloRetryException("發(fā)生Hello異常");
}
return "hello " + nameService.getName();
}
默認情況下箩艺,會重試三次窜醉,重試間隔為1秒。當(dāng)然我們也可以自定義重試次數(shù)和間隔艺谆。這樣就跟我前面實現(xiàn)的功能是一毛一樣的了榨惰。
但Spring里的重試機制還支持很多很有用的特性,比如說静汤,可以指定只對特定類型的異常進行重試琅催,這樣如果拋出的是其它類型的異常則不會進行重試,就可以對重試進行更細粒度的控制虫给。默認為空藤抡,會對所有異常都重試。
@Retryable{value = {HelloRetryException.class}}
public String hello(){2
...
}
也可以使用include和exclude來指定包含或者排除哪些異常進行重試抹估。
可以用maxAttemps指定最大重試次數(shù)缠黍,默認為3次。
可以用interceptor設(shè)置重試攔截器的bean名稱药蜻。
可以通過label設(shè)置該重試的唯一標(biāo)志瓷式,用于統(tǒng)計輸出。
可以使用exceptionExpression來添加異常表達式语泽,在拋出異常后執(zhí)行贸典,以判斷后續(xù)是否進行重試。
此外踱卵,Spring中的重試機制還支持使用backoff來設(shè)置重試補償機制瓤漏,可以設(shè)置重試間隔,并且支持設(shè)置重試延遲倍數(shù)。
舉個例子:
@Retryable(value = {HelloRetryException.class}, maxAttempts = 5,
backoff = @Backoff(delay = 1000, multiplier = 2))
public String hello(){
...
}
該方法調(diào)用將會在拋出HelloRetryException異常后進行重試蔬充,最大重試次數(shù)為5,第一次重試間隔為1s班利,之后以2倍大小進行遞增饥漫,第二次重試間隔為2s,第三次為4s罗标,第四次為8s庸队。
重試機制還支持使用@Recover 注解來進行善后工作,當(dāng)重試達到指定次數(shù)之后闯割,將會調(diào)用該方法彻消,可以在該方法中進行日志記錄等操作。
這里值得注意的是宙拉,想要@Recover 注解生效的話宾尚,需要跟被@Retryable 標(biāo)記的方法在同一個類中,且被@Retryable 標(biāo)記的方法不能有返回值谢澈,否則不會生效煌贴。
并且如果使用了@Recover注解的話,重試次數(shù)達到最大次數(shù)后锥忿,如果在@Recover標(biāo)記的方法中無異常拋出牛郑,是不會拋出原異常的。
@Recover
public boolean recover(Exception e) {
log.error("達到最大重試次數(shù)",e);
return false;
}
除了使用注解外敬鬓,Spring Retry 也支持直接在調(diào)用時使用代碼進行重試:
@Test
public void normalSpringRetry() {
// 表示哪些異常需要重試,key表示異常的字節(jié)碼,value為true表示需要重試
Map<Class<? extends Throwable>, Boolean> exceptionMap = new HashMap<>();
exceptionMap.put(HelloRetryException.class, true);
// 構(gòu)建重試模板實例
RetryTemplate retryTemplate = new RetryTemplate();
// 設(shè)置重試回退操作策略淹朋,主要設(shè)置重試間隔時間
FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
long fixedPeriodTime = 1000L;
backOffPolicy.setBackOffPeriod(fixedPeriodTime);
// 設(shè)置重試策略,主要設(shè)置重試次數(shù)
int maxRetryTimes = 3;
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(maxRetryTimes, exceptionMap);
retryTemplate.setRetryPolicy(retryPolicy);
retryTemplate.setBackOffPolicy(backOffPolicy);
Boolean execute = retryTemplate.execute(
//RetryCallback
retryContext -> {
String hello = helloService.hello();
log.info("調(diào)用的結(jié)果:{}", hello);
return true;
},
// RecoverCallBack
retryContext -> {
//RecoveryCallback
log.info("已達到最大重試次數(shù)");
return false;
}
);
}
此時唯一的好處是可以設(shè)置多種重試策略:
NeverRetryPolicy:只允許調(diào)用RetryCallback一次钉答,不允許重試
AlwaysRetryPolicy:允許無限重試础芍,直到成功,此方式邏輯不當(dāng)會導(dǎo)致死循環(huán)
SimpleRetryPolicy:固定次數(shù)重試策略希痴,默認重試最大次數(shù)為3次者甲,RetryTemplate默認使用的策略
TimeoutRetryPolicy:超時時間重試策略,默認超時時間為1秒砌创,在指定的超時時間內(nèi)允許重試
ExceptionClassifierRetryPolicy:設(shè)置不同異常的重試策略虏缸,類似組合重試策略,區(qū)別在于這里只區(qū)分不同異常的重試
CircuitBreakerRetryPolicy:有熔斷功能的重試策略嫩实,需設(shè)置3個參數(shù)openTimeout刽辙、resetTimeout和delegate
CompositeRetryPolicy:組合重試策略,有兩種組合方式甲献,樂觀組合重試策略是指只要有一個策略允許即可以重試宰缤,
悲觀組合重試策略是指只要有一個策略不允許即可以重試,但不管哪種組合方式,組合中的每一個策略都會執(zhí)行
可以看出慨灭,Spring中的重試機制還是相當(dāng)完善的朦乏,比上面自己寫的AOP切面功能更加強大。
這里還需要再提醒的一點是氧骤,由于Spring Retry用到了Aspect增強呻疹,所以就會有使用Aspect不可避免的坑——方法內(nèi)部調(diào)用,如果被 @Retryable 注解的方法的調(diào)用方和被調(diào)用方處于同一個類中筹陵,那么重試將會失效刽锤。
但也還是存在一定的不足,Spring的重試機制只支持對異常進行捕獲朦佩,而無法對返回值進行校驗并思。
Guava Retry
最后,再介紹另一個重試利器——Guava Retry语稠。
相比Spring Retry宋彼,Guava Retry具有更強的靈活性,可以根據(jù)返回值校驗來判斷是否需要進行重試颅筋。
先來看一個小栗子:
先引入jar包:
<dependency>
<groupId>com.github.rholder</groupId>
<artifactId>guava-retrying</artifactId>
<version>2.0.0</version>
</dependency>
然后用一個小Demo來感受一下:
@Test
public void guavaRetry() {
Retryer<String> retryer = RetryerBuilder.<String>newBuilder()
.retryIfExceptionOfType(HelloRetryException.class)
.retryIfResult(StringUtils::isEmpty)
.withWaitStrategy(WaitStrategies.fixedWait(3, TimeUnit.SECONDS))
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
.build();
try {
retryer.call(() -> helloService.hello());
} catch (Exception e){
e.printStackTrace();
}
}
先創(chuàng)建一個Retryer實例宙暇,然后使用這個實例對需要重試的方法進行調(diào)用,可以通過很多方法來設(shè)置重試機制议泵,比如使用retryIfException來對所有異常進行重試占贫,使用retryIfExceptionOfType方法來設(shè)置對指定異常進行重試,使用retryIfResult來對不符合預(yù)期的返回結(jié)果進行重試先口,使用retryIfRuntimeException方法來對所有RuntimeException進行重試型奥。
還有五個以with開頭的方法,用來對重試策略/等待策略/阻塞策略/單次任務(wù)執(zhí)行時間限制/自定義監(jiān)聽器進行設(shè)置碉京,以實現(xiàn)更加強大的異常處理厢汹。
通過跟Spring AOP的結(jié)合,可以實現(xiàn)比Spring Retry更加強大的重試功能谐宙。
仔細對比之下烫葬,Guava Retry可以提供的特性有:
- 可以設(shè)置任務(wù)單次執(zhí)行的時間限制,如果超時則拋出異常凡蜻。
- 可以設(shè)置重試監(jiān)聽器搭综,用來執(zhí)行額外的處理工作。
- 可以設(shè)置任務(wù)阻塞策略划栓,即可以設(shè)置當(dāng)前重試完成兑巾,下次重試開始前的這段時間做什么事情。
- 可以通過停止重試策略和等待策略結(jié)合使用來設(shè)置更加靈活的策略忠荞,比如指數(shù)等待時長并最多10次調(diào)用蒋歌,隨機等待時長并永不停止等等帅掘。
總結(jié)
本文由淺入深的對多種重試的姿勢進行了360度無死角教學(xué),從最簡單的手動重試堂油,到使用靜態(tài)代理修档,再到JDK動態(tài)代理和CGLib動態(tài)代理,再到Spring AOP称诗,都是手工造輪子的過程萍悴,最后介紹了兩種目前比較好用的輪子,一個是Spring Retry寓免,使用起來簡單粗暴,與Spring框架天生搭配计维,一個注解搞定所有事情袜香,另一個便是Guava Retry,不依賴于Spring框架鲫惶,自成體系蜈首,使用起來更加靈活強大。
個人認為欠母,大部分場景下欢策,Spring Retry提供的重試機制已經(jīng)足夠強大,如果不需要Guava Retry提供的額外靈活性赏淌,使用Spring Retry就很棒了踩寇。當(dāng)然,具體情況具體分析六水,但沒有必要的情況下俺孙,不鼓勵重復(fù)造輪子,先把別人的輪子研究清楚再想想還用不用自己動手掷贾。
本文到此就告一段落了睛榄,又用了一天的時間完成了完成了一篇文章,寫作的目的在于總結(jié)和分享想帅,我相信最佳實踐是可以總結(jié)和積累下來的场靴,在大多數(shù)場景下都是適用的,這些最佳實踐會在逐漸的積累過程中港准,成為比經(jīng)驗更為重要的東西旨剥。因為經(jīng)驗不總結(jié)就會忘記,而總結(jié)出來的內(nèi)容卻不會被丟失叉趣。
如果對于重試你有更好的想法泞边,歡迎提出交流探討,也歡迎關(guān)注我的公眾號進行留言交流疗杉。
轉(zhuǎn)載地址:
【最佳實踐】如何優(yōu)雅的進行重試