编码技巧——重试模板RetryTemplate

本篇介绍Spring RetryTemplate的原理及使用的代码示例;

1. 开发中接口重试的场景

日常开发中,经常会遇到这样的场景:执行一次接口调用,如RPC调用,偶现失败,原因可能是dubbo超时、连接数耗尽、http网络抖动等,出现异常时我们并不能立即知道原因并作出反应,可能只是一个普通的RpcException或RuntimeException,

对于这种小概率的异常,往往需要尝试再次调用(前提是接口是幂等的),因为由于网络问题、下游服务暂时的不稳定导致的异常,段时间后理论上是可以自恢复的;

例如,有时候项目需要进行同步数据,一定要同步成功,不然对于业务会有影响,偶发性的会出现调用接口失败,失败并不是特别多;

一般我们处理偶发异常的流程如下:异常时,

(1)循环的进行远程调用若干次数,记录一下调用失败的记录;
(2)休眠一段时间,尝试等待下游系统自恢复或释放连接数,继续循环调用失败的请求;
(3)如果再调用失败、通过人工二次调用进行修复;

当然,你可以通过写一个指定次数的for循环来执行重试,或者在catch时再次尝试调用一次接口,这么做是可以,但是代码会显得不够优雅;通过前面描述的2种方式,思考下:

对于一个重试模块,要具备哪些属性?

(1)重试时执行的方法,这个方法执行成功后就不用再重试,这个方法要求幂等,并且失败时需要抛出异常来标识执行失败;
(2)重试策略:重试最大次数、重试最大时间即XXms后不再重试、重试次数之间的间隔、对于哪些异常才需要重试;
(3)重试次数达到上限任未成功,最后的兜底逻辑,如插入重试任务表、发送消息给系统管理员等;

2. Spring Retry及示例

Spring retry是Spring提供的一种重试机制的解决方案;Spring retry提供了注解(声明式)和编程两种支持,提供了 RetryTemplate 支持,整个流程如下:

 结合我们上面对一个重试模块应该具备哪些属性的思考,看看这几个属性的作用:

1. RetryTemplate: 封装了Retry基本操作,是进入spring-retry框架的整体流程入口,通过RetryTemplate可以指定监听、回退策略、重试策略等。

2. RetryCallback:该接口封装了业务代码,且failback后,会再次调用RetryCallback接口,直到达到重试次数/时间上限;

3. RecoveryCallback:当RetryCallback不能再重试的时候,如果定义了RecoveryCallback,就会调用RecoveryCallback,并以其返回结果作为最终的返回结果。此外,RetryCallback和RecoverCallback定义的接口方法都可以接收一个RetryContext上下文参数,通过它可以获取到尝试次数、异常,也可以通过其setAttribute()和getAttribute()来传递一些信息。

4. RetryPolicy:重试策略,描述什么条件下可以尝试重复调用RetryCallback接口;策略包括最大重试次数、指定异常集合/忽略异常集合、重试允许的最大超时时间;RetryTemplate内部默认时候用的是SimpleRetryPolicy,SimpleRetryPolicy默认将对所有异常进行尝试,最多尝试3次。还有其他多种更为复杂功能更多的重试策略;

5. BackOffPolicy:回退策略,用来定义在两次尝试之间需要间隔的时间,如固定时间间隔、递增间隔、随机间隔等;RetryTemplate内部默认使用的是NoBackOffPolicy,其在两次尝试之间不会进行任何的停顿。对于一般可重试的操作往往是基于网络进行的远程请求,它可能由于网络波动暂时不可用,如果立马进行重试它可能还是不可用,但是停顿一下,过一会再试可能它又恢复正常了,所以在RetryTemplate中使用BackOffPolicy往往是很有必要的;

6. RetryListener:RetryTemplate中可以注册一些RetryListener,可以理解为是对重试过程中的一个增强,它可以在整个Retry前、整个Retry后和每次Retry失败时进行一些操作;如果只想关注RetryListener的某些方法,则可以选择继承RetryListenerSupport,它默认实现了RetryListener的所有方法;

关于详细的每种策略的功能、使用示例,可以参考这篇文章《Spring Retry介绍》,写的非常细;

代码示例

实际开发中,我们一般使用简单的固定间隔和重试指定次数的策略,代码示例如下:

依赖(一般spring包已经导入了):

        <dependency>
            <groupId>org.springframework.retry</groupId>
            <artifactId>spring-retry</artifactId>
            <version>1.2.4.RELEASE</version>
        </dependency>

自定义的简单重试模板:

/**
 * @author AA
 * @description 重试模板和重试策略
 */
public class RetryTemplate extends org.springframework.retry.support.RetryTemplate {

    private volatile static RetryTemplate instance = null;

    private RetryTemplate() {
    }

    public static RetryTemplate getInstance() {
        if (instance == null) {
            synchronized (RetryTemplate.class) {
                if (instance == null) {
                    FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
                    // 定义重试间隔-间隔100ms再重试,总共重试3次
                    backOffPolicy.setBackOffPeriod(100);
                    instance = new RetryTemplate();
                    // 定义重试次数-固定3次
                    instance.setRetryPolicy(new SimpleRetryPolicy(3));
                    instance.setBackOffPolicy(backOffPolicy);
                }
            }
        }
        return instance;
    }
}

业务代码:

    /**
     * 异步发放权益
     *
     * @param sendBenefitReq
     */
    private void asynSendBenefit(SendBenefitReq sendBenefitReq) {
        try {
            Integer benefitType = sendBenefitReq.getBenefitType();
            ExecutorService executorService = ExecutorManager.getExecutorService(benefitType);
            executorService.submit(
                    () -> {
                        // 默认重试3次,失败后走定时任务补偿
                        RetryTemplate.getInstance().execute(
                                // 重试方法的实现
                                context -> {
                                    this.sendBenefitWithUpdate(sendBenefitReq);
                                    return null;
                                },
                                // 重试方法在重试策略结束后依旧没成功(抛出异常) 则执行下面方法
                                context -> {
                                    Throwable lastThrowable = context.getLastThrowable();
                                    ResultCodeEnum facadeResultEnum = ResultCodeEnum.SERVER_ERROR;
                                    if (lastThrowable != null) {
                                        if (lastThrowable instanceof BusinessException) {
                                            BusinessException businessException = (BusinessException) lastThrowable;
                                            ResultCodeEnum resultCodeEnum = businessException.getCode();
                                            log.warn("The_reason_for_reissuing_the_voucher_is_BuException,code={},message={}", resultCodeEnum.getCode(),
                                                    resultCodeEnum.getDesc());
                                        } else {
                                            log.error("The_reason_for_reissuing_the_voucher_is_other_Exception", lastThrowable);
                                        }
                                    }
                                    // 发放权益失败超过3次后创建补偿定时任务
                                    log.warn("async_sendBenefit_fail_after_3_times_try! intoRetryTask.[sendBenefitReq={}]", sendBenefitReq);
                                    this.intoRetryTask(Collections.singletonList(sendBenefitReq));
                                    throw new BusinessException(facadeResultEnum);
                                });
                    }
            );
        } catch (Exception e) {
            log.error("[benefit receive]retry_task_error! e:{}", e);
        }
    }

下面是一个简单的使用RetryListener的示例:

@Test
public void testListener() {
  RetryTemplate retryTemplate = new RetryTemplate();
  AtomicInteger counter = new AtomicInteger();
  RetryCallback<Integer, IllegalStateException> retryCallback = retryContext -> {
    //内部默认重试策略是最多尝试3次,即最多重试两次。还不成功就会抛出异常。
    if (counter.incrementAndGet() < 3) {
      throw new IllegalStateException();
    }
    return counter.incrementAndGet();
  };

  RetryListener retryListener = new RetryListener() {
    @Override
    public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
      System.out.println("---open----在第一次重试时调用");
      return true;
    }

    @Override
    public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
      System.out.println("close----在最后一次重试后调用(无论成功与失败)。" + context.getRetryCount());
    }

    @Override
    public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
      System.out.println("error----在每次调用异常时调用。" + context.getRetryCount());
    }
  };

  retryTemplate.registerListener(retryListener);
  retryTemplate.execute(retryCallback);

}

3. spring retry 使用注解方式(声明式)

Spring Retry支持对Spring bean使用声明式的重试,在需要重试的bean方法上加上@Retryable;使用这种机制需要在@Configuration类上加上@EnableRetry

(1)@EnableRetry

@EnableRetry
@Configuration
public class RetryConfiguration {

  @Bean
  public HelloService helloService() {
    return new HelloService();
  }

}

(2)@Retryable

通过上面的配置,这样就启用了声明式的重试机制,其会对使用了@Retryable标注的方法对应的bean创建对应的代理。使用@Retryable标注的方法如果不特殊声明的话,默认最多可以尝试3次。

public class HelloService {

  @Retryable
  public void hello(AtomicInteger counter) {
    if (counter.incrementAndGet() < 10) {
      throw new IllegalStateException();
    }
  }

}

说明

1. @Retryable也可以加在Class上,当加在Class上时表示该bean所有的对外方法都是可以重试的。当Class上和方法上都加了@Retryable时,方法上的优先级更高

2. 默认的最大尝试次数是3次,可以通过maxAttempts属性进行自定义;

3. 默认会对所有的异常进行重试,如有需要可以通过value和include属性指定需要重试的异常,也可以通过exclude属性指定不需要进行重试的异常;

4. 可以通过backoff属性指定BackOffPolicy相关的信息,它对应一个@BackOff,默认使用的BackOffPolicy将每次都间隔1000毫秒,如果默认值不能满足要求可以通过@BackOff指定初始的间隔时间,可以通过@BackOff的multiplier属性指定间隔之间的倍数,默认是0,即每次都是固定的间隔时间。当指定了multiplier后可以通过maxDelay属性指定最大的间隔时间,默认是0,表示不限制,即取ExponentialBackOffPolicy的默认值30秒;

示例:

(3)@Recover

@Retryable
public class HelloService {

  @Retryable(maxAttemptsExpression = "${retry.maxAttempts:5}",
          backoff = @Backoff(delayExpression = "${retry.delay:100}",
                  maxDelayExpression = "${retry.maxDelay:2000}",
                  multiplierExpression = "${retry.multiplier:2}"))
  public void hello(AtomicInteger counter) {
    if (counter.incrementAndGet() < 10) {
      throw new IllegalStateException();
    }
  }

  @Recover
  public void helloRecover(AtomicInteger counter) {
    counter.set(1000);
  }

  @Recover
  public void helloRecover(IllegalStateException e, AtomicInteger counter) {
    counter.set(2000);
  }

}

说明:

1. 使用注解的可重试方法,如果重试次数达到后还是继续失败的就会抛出异常,它可以通过@Recover标记同一Class中的一个方法作为RecoveryCallback;

2. @Recover标记的方法的返回类型必须与@Retryable标记的方法一样方法参数可以与@Retryable标记的方法一致,也可以不带参数,带了参数就会传递过来。

3. @Recover标记的方法还可以选择包含一个Exception类型的参数,它对应于@Retryable标记的方法最后抛出的异常,如果需要包含异常参数该参数必须是第一个参数。当定义了多个@Recover方法时,Spring Retry将选择更精确的那一个。例如上面的示例,此时的RecoveryCallback将选择第二个helloRecover方法;

(4)监听器RetryListener

使用声明式的Spring Retry,如果需要使用RetryListener,只需把它们定义为一个Spring bean即可。比如下面:

@EnableRetry
@Configuration
@PropertySource("classpath:/application.properties")
public class RetryConfiguration {

  @Bean
  public HelloService helloService() {
    return new HelloService();
  }

  @Bean
  public RetryListener retryListener() {
    return new RetryListenerSupport() {
      @Override
      public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
        super.onError(context, callback, throwable);
        System.out.println("发生异常:" + context.getRetryCount());
      }
    };
  }

}

参考:

【Spring】RetryTemplate - 简书

Spring RetryTemplate介绍

  • 7
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值