接口重试机制实战

秒速五厘米 2024-03-22 20:59 180阅读 0赞

#

目录

前言

Spring-Retry(重试机制)

一、spring-retry是什么?

二、引入依赖

三、启用@Retryable

四、在方法上添加@Retryable

五、单元测试

六、@Retryable注解中参数的含义:

Guava Retry

一、guava-retrying 是什么

二、引入依赖

三、构建retryer

四、 主逻辑放在callable里,传给retryer进行调用

五、guava-retrying支持多种条件下重试,来具体看看。

retryIfException()

retryIfRuntimeException()

retryIfExceptionOfType


前言

当我们单体应用时,所有的逻辑计算都在单一的进程中,除了进程断电外几乎不可能有处理失败的情况。然而,当我们把单体应用拆分为一个个细分的子服务后,服务间的互相调用无论是RPC还是HTTP,都是依赖于网络。

网络是脆弱的,不时请求会出现抖动失败。例如我们的 网关Gateway 调用 订单微服务 进行下单时,可能网络超时了,这个时候 网关Gateway 就需要返回给用户提示「网络错误」,这样我们的服务质量就下降了,可能会收到用户的投诉吐槽,降低产品竞争力。

对于网络抖动这种情况,解决的最简单办法之一就是重试


Spring-Retry(重试机制)

在实际工作中,重处理是一个非常常见的场景,比如:

发送消息失败,保存Redis失败。 调用远程服务失败。 支付成功回调失败。 这些错误可能是因为网络波动造成的,等待过后重处理就能成功。通常来说,会用try/catch,while循环之类的语法来进行重处理,但是这样的做法缺乏统一性,会侵入业务代码,难以维护。spring-retry却可以通过注解,在不入侵原有业务逻辑代码的方式下,优雅的实现重处理功能,使业务代码和重处理解耦。

一、spring-retry是什么?

spring系列的spring-retry是另一个实用程序模块,可以帮助我们以标准方式处理任何特定操作的重试。在spring-retry中,所有配置都是基于注解的。

二、引入依赖

基于AOP实现需引入aop相关的依赖

  1. <dependency>
  2. <groupId>org.springframework.retry</groupId>
  3. <artifactId>spring-retry</artifactId>
  4. <version>1.2.4.RELEASE</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.springframework.boot</groupId>
  8. <artifactId>spring-boot-starter-aop</artifactId>
  9. <version>2.3.4.RELEASE</version>
  10. </dependency>

三、启用@Retryable

  1. /**
  2. * 启动类
  3. *
  4. * @author yangyanping
  5. * @date 2022-11-17
  6. */
  7. @Slf4j
  8. @EnableRetry
  9. @EnableDynamicConfigEvent
  10. @EnableTransactionManagement
  11. @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
  12. public class WebApplication {
  13. public static void main(String[] args) {
  14. SpringApplication.run(WebApplication.class, args);
  15. log.info(">>>>>>>>>>>workId={}", System.getProperty("workId"));
  16. }
  17. }

四、在方法上添加@Retryable

定义接口UserService

  1. public interface UserService {
  2. String getUserInfo(String userId);
  3. }

定义接口实现类 UserServiceImpl ,使用@Retryable

  1. @Slf4j
  2. @Service
  3. public class UserServiceImpl implements UserService {
  4. /**
  5. * value:抛出指定异常才会重试
  6. * include:和value一样,默认为空,当exclude也为空时,默认所有异常
  7. * exclude:指定不处理的异常
  8. * maxAttempts:最大重试次数,默认3次
  9. * backoff:重试等待策略,
  10. * 默认使用@Backoff,@Backoff的value默认为1000L,我们设置为2000; 以毫秒为单位的延迟(默认 1000)
  11. * multiplier(指定延迟倍数)默认为0,表示固定暂停1秒后进行重试,如果把multiplier设置为1.5,则第一次重试为2秒,第二次为3秒,第三次为4.5秒。
  12. */
  13. @Override
  14. @Retryable(value = Exception.class, maxAttempts = 3, backoff = @Backoff(delay = 2000, multiplier = 1.5))
  15. public String getUserInfo(String userId) {
  16. log.info("getUserInfo#userId={}", userId);
  17. throw ExceptionFactory.bizException("-1", "接口异常");
  18. }
  19. /**
  20. * Spring-Retry还提供了@Recover注解,用于@Retryable重试失败后处理方法。
  21. * 如果不需要回调方法,可以直接不写回调方法,那么实现的效果是,重试次数完了后,如果还是没成功没符合业务判断,就抛出异常。
  22. * 可以看到传参里面写的是 Exception e,这个是作为回调的接头暗号(重试次数用完了,还是失败,我们抛出这个Exception e通知触发这个回调方法)。
  23. * 注意事项:
  24. * 方法的返回值必须与@Retryable方法一致
  25. * 方法的第一个参数,必须是Throwable类型的,建议是与@Retryable配置的异常一致,其他的参数,需要哪个参数,写进去就可以了(@Recover方法中有的)
  26. * 该回调方法与重试方法写在同一个实现类里面
  27. * <p>
  28. * 由于是基于AOP实现,所以不支持类里自调用方法
  29. * 如果重试失败需要给@Recover注解的方法做后续处理,那这个重试的方法不能有返回值,只能是void
  30. * 方法内不能使用try catch,只能往外抛异常
  31. *
  32. * @param e
  33. * @param userId
  34. * @return
  35. * @Recover注解来开启重试失败后调用的方法(注意,需跟重处理方法在同一个类中),此注解注释的方法参数一定要是@Retryable抛出的异常,否则无法识别,可以在该方法中进行日志处理。
  36. */
  37. @Recover
  38. public String recover(Exception e, String userId) {
  39. log.info("回调方法执行,recover#userId={}",userId);
  40. //记日志到数据库 或者调用其余的方法
  41. log.info("异常信息:" + e.getMessage());
  42. return userId;
  43. }
  44. }

五、单元测试

  1. @Slf4j
  2. @RunWith(SpringRunner.class)
  3. @SpringBootTest(classes = WebApplication.class)
  4. public class BaseTest {
  5. @Resource
  6. private UserService userService;
  7. @Test
  8. public void getUserInfo(){
  9. userService.getUserInfo("123");
  10. }
  11. }

运行结果:

  1. [2023/07/26 20:21:13.948][INFO][UserServiceImpl:25] getUserInfo#userId=123
  2. [2023/07/26 20:21:15.950][INFO][UserServiceImpl:25] getUserInfo#userId=123
  3. [2023/07/26 20:21:18.955][INFO][UserServiceImpl:25] getUserInfo#userId=123
  4. [2023/07/26 20:21:18.958][INFO][UserServiceImpl:50] 回调方法执行,recover#userId=123
  5. [2023/07/26 20:21:18.958][INFO][UserServiceImpl:52] 异常信息:接口异常

六、@Retryable注解中参数的含义:

从中挑出一些重要的属性说明










































属性名称 说明
interceptor 重试方法使用的重试拦截器bean名称,和其他的属性互斥(哪个优先待确认)
value 哪些异常可以触发重试 ,是include的同义词,复制将会应用到include,默认为空
include 哪些异常可以触发重试 ,默认为空
exclude 哪些异常将不会触发重试,默认为空,如果和include属性同时为空,则所有的异常都将会触发重试
stateful 是否是无状态
maxAttempts 重试策略之最大尝试次数,默认3次
maxAttemptsExpression 字面意思是使用表达式来提供最大重试次数,默认3次
backoff 重试等待策略,默认使用@Backoff,@Backoff的value默认为1000(单位毫秒),我们设置为2000;multiplier(指定延迟倍数)默认为0,表示固定暂停1秒后进行重试,如果把multiplier设置为1.5,则第一次重试为2秒,第二次为3秒,第三次为4.5秒

Spring-Retry还提供了@Recover注解,用于@Retryable重试失败后处理方法。如果不需要回调方法,可以直接不写回调方法,那么实现的效果是,重试次数完了后,如果还是没成功没符合业务判断,就抛出异常。

  1. @Recover
  2. public String recover(Exception e, String userId) {
  3. log.info("回调方法执行,recover#userId={}",userId);
  4. //记日志到数据库 或者调用其余的方法
  5. log.info("异常信息:" + e.getMessage());
  6. return userId;
  7. }

#

#

Guava Retry

一、guava-retrying 是什么

guava-retrying是Google Guava库的一个扩展包,可以为任意函数调用创建可配置的重试机制。该扩展包比较简单,大约包含了10个方法和类

二、引入依赖

  1. <dependency>
  2. <groupId>com.github.rholder</groupId>
  3. <artifactId>guava-retrying</artifactId>
  4. <version>2.0.0</version>
  5. </dependency>

三、构建retryer

Retryer是最核心的类,是用于执行重试策略的类,通过RetryerBuilder类进行构造,并且RetryerBuilder负责将设置好的重试策咯添加到Retryer中,最终通过执行Retryer的核心方法call来执行重试策略(一次任务的执行是如何进行的?)

  1. Retryer<Integer> retryer = RetryerBuilder.<Integer>newBuilder()
  2. .retryIfException()
  3. .withStopStrategy(StopStrategies.stopAfterAttempt(5))
  4. .withWaitStrategy(WaitStrategies.fixedWait(3, TimeUnit.SECONDS))
  5. .build();

四、 主逻辑放在callable里,传给retryer进行调用

  1. public class RetryTest {
  2. public static void main(String[] args) {
  3. mock();
  4. }
  5. private static Retryer<Integer> retryer = RetryerBuilder.<Integer>newBuilder()
  6. .retryIfException()
  7. .withStopStrategy(StopStrategies.stopAfterAttempt(5))
  8. .withWaitStrategy(WaitStrategies.fixedWait(3, TimeUnit.SECONDS))
  9. .build();
  10. public static int mock() {
  11. Callable<Integer> callable =() -> doQuery();
  12. int result;
  13. try {
  14. result = retryer.call(callable);
  15. } catch (Exception e) {
  16. result = -1;
  17. }
  18. return result;
  19. }
  20. private static int doQuery() {
  21. Random r = new Random(System.currentTimeMillis());
  22. int num = r.nextInt(5);
  23. System.out.println("query result " + num);
  24. if (num == 0) {
  25. return 0;
  26. } else if (num == 1) {
  27. System.out.println("DBException");
  28. throw ExceptionFactory.bizException("-1", "bizException");
  29. } else if (num == 2) {
  30. System.out.println("IllegalArgumentException");
  31. throw new IllegalArgumentException("IllegalArgumentException");
  32. } else if (num == 3) {
  33. System.out.println("NullPointerException");
  34. throw new NullPointerException("NullPointerException");
  35. } else {
  36. System.out.println("IndexOutOfBoundsException");
  37. throw new IndexOutOfBoundsException("IndexOutOfBoundsException");
  38. }
  39. }
  40. }

运行结果

query result 3
NullPointerException
query result 0

进程已结束,退出代码0

五、guava-retrying支持多种条件下重试,来具体看看。

retryIfException()

这个就是在任何异常发生时,都会进行重试。上面的例子中已经用到。

retryIfRuntimeException()

这个是指,只有runtime exception发生时,才会进行重试。

retryIfExceptionOfType

发生某种指定异常时,才重试。例如

  1. .retryIfExceptionOfType(DBException.class)

发表评论

表情:
评论列表 (有 0 条评论,180人围观)

还没有评论,来说两句吧...

相关阅读

    相关 dubbo机制

    默认是重试2次,加上初始一次,总共调用提供者3次;为了提高数据的幂等性,建议项目中取消重试配置; springboot+dubbo的配置如下 消费者: spring

    相关 RabbitMQ机制

    1、RabbitMQ重试机制的简介 RabbitMQ 不会为未确认的消息设置过期时间,它判断此消息是否需要重新投递给消费者的唯一依据是消费该消息连接是否已经断开,这个设置

    相关 Retry机制

    1、业务场景        应用中需要实现一个功能: 需要将数据上传到远程存储服务,同时在返回处理成功情况下做其他操作。这个功能不复杂,分为两个步骤:第一步调用远程的Rest