微服务之间互相调用,一般情况下都会设置一些兜底手段,避免服务出现问题,最常见方案就是接口重试机制。
对于接口重试常见的方案有:
1、 硬核捕获;
2、Spring AOP 实现;
3、Spring 自带重试工具;
4、Gavua 提供重试工具。
一、准备工作
1、提供一个接口,该接口用来模拟出现网络波动时,服务调用失败的情况:
@Component
public class RetryServcie {
private LongAdder num = new LongAdder();
/**
* @Desc 模拟服务调用异常
* @Author jidi
* @date 2022/4/9 20:19
* @return java.lang.String
*/
public String hello(){
num.increment();
long l = num.longValue();
if(l < 3L){
throw new RuntimeException("出错了哟!");
}else {
num.reset();
return "接口调用成功";
}
}
}
2、编写对应的 Controller:
@RestController
@RequestMapping("/retry")
public class RetryController {
@Autowired
private RetryServcie retryServcie;
@GetMapping(value = "/test")
public Result retry(){
return Result.ok().setData(retryServcie.hello());
}
}
二、硬核捕获
当调用服务异常或者不可用时,直接使用异常捕获,不进行重试(或者返回一个默认值)。
@GetMapping(value = "/test")
public Result retry(){
String hello;
try {
hello = retryServcie.hello();
} catch (Exception e) {
log.error("调用第三方接口失败:{}", e);
return Result.exception().setData("调用第三方接口失败!");
}
return Result.ok().setData(hello);
}
三、Spring Aop实现
- 自定义重试注解:
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Retryable {
// 重试次数
int retryTimes() default 3;
// 重试时间间隔
int retryInterval() default 1;
}
- 定义切面处理逻辑:
@Slf4j
@Aspect
@Component
public class RetryableAspect {
@Pointcut("@annotation(com.jidi.springbootredis.annotation.Retryable)")
public void pointcut(){}
@Around(value = "pointcut() && @annotation(retryable)")
public Object around(ProceedingJoinPoint point, Retryable retryable) throws Throwable {
int interval = retryable.retryInterval();
int times = retryable.retryTimes();
// 默认返回结果为 null
Object result;
for (int retryTime = 1; retryTime <= times; retryTime++) {
try {
result = point.proceed();
// 只要成功一次,跳出循环(根据具体业务判断是否成功)
if(Objects.nonNull(result)){
return result;
}
} catch (Exception e) {
log.error("第" + retryTime + "次重试,失败:{}", e);
}
// 等待指定时间,每次延后100ms
Thread.sleep(interval * 100);
}
// 次数用完(可根据实际业务返回)
throw new RuntimeException("重试次数用完了!");
}
}
- 使用自定义的注解:
@Retryable(retryTimes = 3,retryInterval = 1)
@GetMapping(value = "/test")
public Result retry(){
String hello = retryServcie.hello();
return Result.ok().setData(hello);
}
四、Spring 自带重试工具
- 引入pom:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<scope>test</scope>
</dependency>
- 在启动类或者配置类通过注解
@EnableRetry
,启用Spring 自带的重试机制; - 在要调用的方法上面通过注解
@Retryable
即可拥有 Spring 自带的重试能力。
@Retryable(value = { RuntimeException.class }, maxAttemptsExpression = "${retry.maxAttempts:10}",
backoff = @Backoff(delayExpression = "${retry.backoff:1000}"))
public String hello(){
num.increment();
long l = num.longValue();
if(l < 5L){
throw new RuntimeException("出错了哟!");
}else {
num.reset();
return "接口调用成功";
}
}
// 重试完成后还是不成功的情况下,会执行被@Recover修饰的方法。
@Recover
public void recover(RuntimeException t) {
log.info("SampleRetryService.recover:{}", t.getClass().getName());
}
1、@Retryable
@Retryable
修饰在要重试的方法上,有以下参数:
interceptor
:重试拦截器bean名称,用于可重试方法;value
:可重试的异常类型。含义同 include。默认为空(如果excludes也为空,则重试所有异常);include
:可重试的异常类型。默认为空(如果excludes也为空,则重试所有异常);exclude
:无需重试的异常类型。默认为空(如果includes也为空,则重试所有异常);label
:统计报告的唯一标签。如果未提供,则调用者可以选择忽略它或提供一个默认值;stateful
:若为true,标志重试是有状态的:即重新抛出异常,但是重试策略与相同的策略应用于具有相同参数的后续调用。若为false,则不会重新引发可重试的异常;maxAttempts
:最大重试次数(包括第一次失败),默认为3次;maxAttemptsExpression
:计算最大尝试次数(包括第一次失败)的表达式,默认为3 次;backoff
:重试等待策略;exceptionExpression
:指定在SimpleRetryPolicy.canRetry()返回true之后要求值的表达式-可用于有条件地禁止重试。
2、@Backoff
@Backoff
,重试等待策略。有以下参数:
value
:倒退时期;delay
:重试之间的等待时间(以毫秒为单位);maxDelay
:重试之间的最大等待时间(以毫秒为单位);multiplier
:指定延迟的倍数;delayExpression
:重试之间的等待时间表达式;maxDelayExpression
:重试之间的最大等待时间表达式;multiplierExpression
:指定延迟的倍数表达式;random
:随机指定延迟时间。
3、@Recover
当重试耗尽时,用于@Retryable
重试失败后处理方法,此方法里的异常一定要是 @Retryable
方法里抛出的异常,否则不会调用这个方法。
@Retryable
和@Recover
修饰的方法要在同一个类中,且被@Recover
标记的方法的返回值必须与@Retryable
方法一致,方法的第一个参数,必须是Throwable
类型的,建议是与@Retryable
配置的异常一致,其他的参数,需要与 @Retryable
方法的参数一致。
由于@Retryable
注解是通过切面实现的,因此要避免@Retryable
注解的方法的调用方和被调用方处于同一个类中,因为这样会使@Retryable
注解失效。
五、Gavua 重试
- 导入 pom:
<dependency>
<groupId>com.github.rholder</groupId>
<artifactId>guava-retrying</artifactId>
<version>2.0.0</version>
</dependency>
- 创建一个Retryer实例:
@GetMapping(value = "/test4")
public Result retry4(){
Result result = null;
// 设置重试策略
Retryer<Result> retryer = RetryerBuilder.<Result>newBuilder()
// 遇到异常重试
.retryIfException()
// 等待时间
.withWaitStrategy(WaitStrategies.fixedWait(1, TimeUnit.SECONDS))
// 重试的终止策略,尝试 2 次后终止
.withStopStrategy(StopStrategies.stopAfterAttempt(2))
.build();
try {
result = retryer.call(()->{
return Result.ok().setData(retryServcie.hello());
});
} catch (Exception e) {
log.error("重试次数耗尽");
result = Result.exception().setData("重试次数耗尽");
}
return result;
}
1、RetryerBuilder
RetryerBuilder
是用来快速生成Retryer实例,并且可以配置重试次数、超时时间等。
2、retryIfException
retryIfException
支持Exception异常对象,当抛出runtime异常、checked异常时都会重试,但是error不会重试。
3、retryIfRuntimeException
retryIfRuntimeException
只会在抛出Runtime异常的时候才会重试,checked异常和error都不重试。
4、retryIfExceptionOfType
retryIfExceptionOfType
允许在发生特定异常的时候才重试,比如NullPointerException和IllegalStateException都属于runtime异常,也包括自定义的异常。
.retryIfExceptionOfType(IllegalStateException.class)
.retryIfExceptionOfType(NullPointerException.class)
5、retryIfResult
retryIfResult
可以指定的Callable方法在返回值的时候进行重试,如 :
.retryIfResult(Predicates.equalTo(false)) // 返回false重试
6、WaitStrategy
等待时长策略,重试间隔时间。常用的策略有:
FixedWaitStrategy
:固定等待时长策略;
withWaitStrategy(WaitStrategies.fixedWait(1, TimeUnit.SECONDS);
RandomWaitStrategy
:随机等待时长策略(可以提供一个最小和最大时长,等待时长为其区间随机值);IncrementingWaitStrategy
:递增等待时长策略(提供一个初始值和步长,等待时间随重试次数增加而增加)。
7、StopStrategy
停止重试策略,提供三种:
StopAfterDelayStrategy
:设定一个最长允许的执行时间;比如设定最长执行10s,无论任务执行次数,只要重试的时候超出了最长时间,则任务终止,并返回重试异常RetryException
。
.withStopStrategy(StopStrategies.stopAfterDelay(5, TimeUnit.SECONDS)) ;
StopAfterAttemptStrategy
:设定最大重试次数,如果超出最大重试次数则停止重试,并返回重试异常。
.withStopStrategy(StopStrategies.stopAfterAttempt(5));
NeverStopStrategy
:不停止,用于需要一直轮询知道返回期望结果的情况。
8、RetryListener
自定义重试监听器,可以用于异步记录错误日志。当发生重试的时候,需要记录下重试的次数、结果等信息,或者有更多的拓展。
public class MyRetryListener implements RetryListener {
@Override
public <String> void onRetry(Attempt<String> attempt) {
// 距离上一次重试的时间间隔
System.out.println("距上一次重试的间隔时间为:" + attempt.getDelaySinceFirstAttempt());
// 重试次数
System.out.println("重试次数: " + attempt.getAttemptNumber());
// 重试过程是否有异常
System.out.println("重试过程是否有异常:" + attempt.hasException());
if (attempt.hasException()) {
System.out.println("异常的原因:" + attempt.getExceptionCause().toString());
}
//重试正常返回的结果
System.out.println("重试结果为:" + attempt.hasResult());
}
}