重试业务场景
给你一个场景,假设你负责支付服务,需要对接外部的一个渠道,调用他们的订单查询接口。他们给你说:由于网络问题,如果我们之间交互超时了,你没有收到我的任何响应,那么按照约定你可以对这个接口发起三次重试,三次之后还是没有响应,那就应该是有问题了,你们按照异常流程处理就行。假设你不知道 Spring-retry 这个组件,那么你大概率会写出这样的代码:
调用之后,日志的输出是这样的,一目了然,非常清晰:
正常调用一次,重试三次,一共可以调用 4 次。在第五次调用的时候抛出异常。完全符合需求,自测也完成了,可以直接提交代码,交给测试同学了。非常完美,但是你有没有想过,这样的代码其实非常的不优雅。
重试应该是一个工具类一样的通用方法,是可以抽离出来的,剥离到业务代码之外,开发的时候我们只需要关注业务代码写的巴巴适适就行了。github.com/spring-proj…
用上 spring-retry 之后,我们上面的代码就变成了这样:
只是加上了一个 @Retryable 注解,这玩意简直简单到令人发指。
基本使用Demo
里面涉及到三个注解:
- @EnableRetry:加在启动类上,表示支持重试功能。
- @Retryable:加在方法上,就会给这个方法赋能,让它有用重试的功能。
@Recover:重试完成后还是不成功的情况下,会执行被这个注解修饰的方法。
maven依赖
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>1.3.1</version>
</dependency>
由于该组件是依赖于 AOP 给你的,所以还需要引入这个依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.6.1</version>
</dependency>
启动类
@SpringBootApplication
@EnableRetry
@EnableRetry(proxyTargetClass = true)
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
业务代码
供了注解式开发和编程式开发的示例。我们这里主要看它的注解式开发案例: ```java @Service @Slf4j public class DoRetryService {
@Retryable(value = Exception.class, maxAttempts = 4, backoff = @Backoff(delay = 2000L, multiplier = 1.5)) public boolean doRetry(boolean isRetry) throws Exception {
log.info("开始通知下游系统");
log.info("通知下游系统");
if (isRetry) {
throw new RuntimeException("通知下游系统异常");
}
return true;
} }
- interceptor:可以通过该参数,指定方法拦截器的bean名称
- value:抛出指定异常才会重试
- include:和value一样,默认为空,当exclude也为空时,默认所以异常
- exclude:指定不处理的异常
- maxAttempts:最大重试次数,默认3次
- backoff:重试等待策略,默认使用@Backoff,@Backoff的value默认为1000L,我们设置为2000L;multiplier(指定延迟倍数)默认为0,表示固定暂停1秒后进行重试,如果把multiplier设置为1.5,则第一次重试为2秒,第二次为3秒,第三次为4.5秒。
最后把项目跑起来,调用一笔,确实是生效了,执行了 @Recover 修饰的方法<br /> Spring-retry 的重试策略, 简单的介绍一下其中的几种含义是啥:
- AlwaysRetryPolicy:允许无限重试,直到成功,此方式逻辑不当会导致死循环
- NeverRetryPolicy:只允许调用RetryCallback一次,不允许重试
- SimpleRetryPolicy:固定次数重试策略,默认重试最大次数为3次,RetryTemplate默认使用的策略
- TimeoutRetryPolicy:超时时间重试策略,默认超时时间为1秒,在指定的超时时间内允许重试
- ExceptionClassifierRetryPolicy:设置不同异常的重试策略,类似组合重试策略,区别在于这里只区分不同异常的重试
- CircuitBreakerRetryPolicy:有熔断功能的重试策略,需设置3个参数openTimeout、resetTimeout和delegate
- CompositeRetryPolicy:组合重试策略,有两种组合方式,乐观组合重试策略是指只要有一个策略允许即可以重试,悲观组合重试策略是指只要有一个策略不允许即不可以重试,但不管哪种组合方式,组合中的每一个策略都会执行
<a name="DfrZW"></a>
## 指定@Recover方法
spring-retry再1.3.0版中@Retryable增加recover属性,从而使得再异常重试失败后可以指定补偿方法执行。从而使得我们不需要每一个用到@Recover的地方都需要新建一个类。
```java
@Retryable(recover = "compensateHi")
@Override
public Hello hi(String name) {
try {
Hello hello = helloClient.hi(name);
return hello;
}catch (Exception e){
e.printStackTrace();
throw new MyHttpExcetption("");
}
}
@Recover
//Throwable throwable必须写,否则无法匹配
private Hello compensateHi(Throwable throwable,String name) {
System.out.println("compensateHi");
try {
Hello hello = helloRepository.findByName(name);
return hello;
}catch (Exception e){
throw new MyHttpExcetption("");
}
}
@Recover
//Throwable throwable必须写,否则无法匹配
private Hello compensateHi2(Throwable throwable,String name) {
System.out.println("compensateHi2");
try {
Hello hello = helloRepository.findByName(name);
return hello;
}catch (Exception e){
throw new MyHttpExcetption("");
}
}
但是要注意的是,在异常重试失败进行匹配是,方法的第一个参数必须是Throwable或其子类,否则就算@Retryable中recover属性指定也无法匹配。
- @Recover的注解的方法第一个参数需要是 @Retryable 所捕获的重试异常类型;必须是第一个参数
- @Recover注解的方法的 返回值必须 和 @Retryable注解的方法返回值一致
注意:
但是这里也暴露了一个 Spring-retry 的弊端,就是必须要通过抛出异常的方式来触发相关业务。
听着好像也是没有毛病,但是你想想一下,假设渠道方说如果我给你返回一个 500 的 ErrorCode,那么你也可以进行重试。这样的业务场景应该也是比较多的。如果你要用 Spring-retry 会怎么做?是不是得写出这样的代码:
if(errorCode==500){
throw new Exception("手动抛出异常");
}
注解属性含义Retryable
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Retryable {
/**
* 为重试方法应用重试拦截器的bean名称。与其他属性互斥
*/
String interceptor() default "";
/**
* 可以重试的异常类型。与includes属性同义。默认值为空(并且如果exclude也是空的话,
* 所有的异常都会重试)
*/
Class<? extends Throwable>[] value() default {};
/**
* 同上
*/
Class<? extends Throwable>[] include() default {};
/**
* 与include含义相反
*/
Class<? extends Throwable>[] exclude() default {};
/**
* 统计报告的唯一标签。如果没有提供,调用者可以选择忽略它,或者提供一个默认值。
*
* @return the label for the statistics
*/
String label() default "";
/**
* 标识重试有状态的:即异常重新抛出,但是重试策略使用相同的策略应用于后续的具有相同参数的
* 调用。如果为false那么可重试的异常不会重新抛出。
*/
boolean stateful() default false;
/**
* 尝试的最大次数(包含第一次失败),默认为3
*/
int maxAttempts() default 3;
/**
* 返回一个求尝试最大次数值的表达式(包含第一次失败),默认为3
* 重写 {@link #maxAttempts()}。
* @since 1.2
*/
String maxAttemptsExpression() default "";
/**
* 为正重试的动作指定backoff属性。默认没有backoff,但是在两次尝试之间暂定一下是一个很好的想法
* (即使代价是阻塞线程)
*/
Backoff backoff() default @Backoff();
/**
* 在{@code SimpleRetryPolicy.canRetry()}返回true之后指定一个计算表达式 - 可用来有条件的取消重试。
* 仅在调用抛出一个异常后。求值的root对象为上一次的异常 {@code Throwable}。
* 可以引用上下文中的其他beans。
* 例如:
* {@code "message.contains('you can retry this')"}.
* and
* {@code "@someBean.shouldRetry(#root)"}.
* @since 1.2
*/
String exceptionExpression() default "";
}
接着我们下面来看看exceptionExpression, 一样也是写SpEL表达式
@Retryable(value = IllegalAccessException.class, exceptionExpression = "message.contains('test')")
public void service4(String exceptionMessage) throws IllegalAccessException {
log.info("do something... {}", LocalDateTime.now());
throw new IllegalAccessException(exceptionMessage);
}
@Retryable(value = IllegalAccessException.class, exceptionExpression = "#{message.contains('test')}")
public void service4_3(String exceptionMessage) throws IllegalAccessException {
log.info("do something... {}", LocalDateTime.now());
throw new IllegalAccessException(exceptionMessage);
}
上面的表达式exceptionExpression = “message.contains(‘test’)”的作用其实是获取到抛出来exception的message(调用了getMessage()方法),然后判断message的内容里面是否包含了test字符串,如果包含的话就会执行重试。所以如果调用方法的时候传入的参数exceptionMessage中包含了test字符串的话就会执行重试。
但这里值得注意的是, Spring Retry 1.2.5之后exceptionExpression是可以省略掉#{…}
还可以在表达式中执行一个方法,前提是方法的类在spring容器中注册了,@retryService其实就是获取bean name为retryService的bean,然后调用里面的checkException方法,传入的参数为#root,它其实就是抛出来的exception对象。一样的也是可以省略#{…}
@Retryable(value = IllegalAccessException.class, exceptionExpression = "#{@retryService.checkException(#root)}")
public void service5(String exceptionMessage) throws IllegalAccessException {
log.info("do something... {}", LocalDateTime.now());
throw new IllegalAccessException(exceptionMessage);
}
@Retryable(value = IllegalAccessException.class, exceptionExpression = "@retryService.checkException(#root)")
public void service5_1(String exceptionMessage) throws IllegalAccessException {
public boolean checkException(Exception e) {
log.error("error message:{}", e.getMessage());
return true; //返回true的话表明会执行重试,如果返回false则不会执行重试
https://juejin.cn/post/7094613787973517343
详细使用:https://blog.csdn.net/u010597819/article/details/108301369
详细使用:https://www.jb51.net/article/245471.htm
源码解析:https://juejin.cn/post/7054024739001466916