优雅实现接口重试

微服务之间互相调用,一般情况下都会设置一些兜底手段,避免服务出现问题,最常见方案就是接口重试机制。

对于接口重试常见的方案有:
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实现

  1. 自定义重试注解:
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Retryable {

    // 重试次数
    int retryTimes() default 3;


    // 重试时间间隔
    int retryInterval() default 1;

}
  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("重试次数用完了!");
    }
}
  1. 使用自定义的注解:
@Retryable(retryTimes = 3,retryInterval = 1)
@GetMapping(value = "/test")
public Result retry(){
    String hello = retryServcie.hello();
    return Result.ok().setData(hello);
}

四、Spring 自带重试工具

  1. 引入pom:
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>

<dependency>
       <groupId>org.springframework.retry</groupId>
       <artifactId>spring-retry</artifactId>
       <scope>test</scope>
</dependency>
  1. 在启动类或者配置类通过注解 @EnableRetry,启用Spring 自带的重试机制;
  2. 在要调用的方法上面通过注解 @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 重试

  1. 导入 pom:
<dependency>
     <groupId>com.github.rholder</groupId>
     <artifactId>guava-retrying</artifactId>
     <version>2.0.0</version>
</dependency>
  1. 创建一个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());
    }
}
  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值