当前位置:   article > 正文

07. HTTP接口请求重试怎么处理?

07. HTTP接口请求重试怎么处理?

 目录

1、前言

2、实现方式

2.1、循环重试

2.2、递归重试

2.3、Spring Retry

2.4、Resilience4j

2.5、http请求网络工具内置重试方式

2.6、自定义重试工具

2.7、并发框架异步重试

2.8、消息队列

3、小结


1、前言

HTTP接口请求重试是指在请求失败时,再次发起请求的机制。在实际应用中,由于网络波动、服务器故障等原因,HTTP接口请求可能会失败。为了保证系统的可用性和稳定性,需要对HTTP接口请求进行重试。

2、实现方式

今天给大家分享一些常见的接口请求重试的方式。本地模拟了一个请求接口,后面的代码示例均模拟请求该接口:

  1. @GetMapping("http_test")
  2. public String getHttpTest(){
  3. return "接口请求成功,返回:OK";
  4. }

2.1、循环重试

循环重试是最简单最粗暴的方式,就是在请求接口代码中加入循环机制,如果接口请求失败,则循环继续发起接口请求,直到请求成功或接口重试次数达到上限。如果请求成功,则不进行重试。

简单代码示例如下:

  1. @GetMapping("retry_demo_loop")
  2. public String retry_demo_loop(){
  3. // 重试上限次数为3次
  4. int maxRetryTime = 3;
  5. String result = null;
  6. // 接口循环请求
  7. for (int i = 1; i <= maxRetryTime; i++) {
  8. try {
  9. // 模拟请求接口
  10. result = HttpUtil.get("http://localhost:8080/http_test");
  11. // 模拟一次请求失败
  12. if(i == 1){
  13. int co = i / 0;
  14. }
  15. // 请求成功,跳出循环
  16. break;
  17. } catch (Exception e) {
  18. log.error("接口请求异常,进行第{}次重试", i);
  19. result = "接口请求失败,请联系管理员";
  20. }
  21. }
  22. return result;
  23. }

请求结果:

重试日志打印:

2.2、递归重试

除了循环,还可以使用递归来实现接口的请求重试。递归是我们都比较熟悉的编程技巧,在请求接口的方法中调用自身,如果请求失败则继续调用,直到请求成功或达到最大重试次数。

  1. @GetMapping("retry_demo_rec")
  2. public String retry_demo_rec(){
  3. // 重试上限次数为3次
  4. int maxRetryTime = 3;
  5. return retryRequest(maxRetryTime);
  6. }
  7. /**
  8. * 递归方法
  9. * @param maxRetryTime
  10. * @return
  11. */
  12. private String retryRequest(int maxRetryTime){
  13. if (maxRetryTime <= 0) {
  14. return "接口请求失败,请联系管理员";
  15. }
  16. int retryTime = 0;
  17. try {
  18. // 模拟请求接口
  19. String result = HttpUtil.get("http://localhost:8080/http_test");
  20. // 模拟一次请求失败
  21. if(maxRetryTime == 3){
  22. int co = 1 / 0;
  23. }
  24. return result;
  25. } catch (Exception e) {
  26. // 处理异常
  27. log.error("接口请求异常,进行第{}次重试", ++retryTime);
  28. return retryRequest(maxRetryTime - 1);
  29. }
  30. }

请求结果:

重试日志打印:

2.3、Spring Retry

第三种便是使用Spring Retry依赖实现。首先我们需要集成相关依赖:

  1. <dependency>
  2. <groupId>org.springframework.retry</groupId>
  3. <artifactId>spring-retry</artifactId>
  4. </dependency>
  5. <!-- 由于retry使用到了aop,所以还需要加入aop依赖 -->
  6. <dependency>
  7. <groupId>org.springframework.boot</groupId>
  8. <artifactId>spring-boot-starter-aop</artifactId>
  9. </dependency>

加入@EnableRetry启动:

  1. @EnableRetry
  2. @SpringBootApplication
  3. public class Application {
  4. public static void main(String[] args) {
  5. SpringApplication.run(Application.class, args);
  6. }
  7. }

添加retry方法注解:

  1. public interface MyRetryService {
  2. /**
  3. * retryable注解表示该方法需要重试
  4. * value:出现该指定异常后,进行重试
  5. * maxAttempts:重试次数上限,这里指定为3次
  6. * backoff:重试策略,这里指定200ms间隔一次
  7. * @param code
  8. * @return
  9. * @throws Exception
  10. */
  11. @Retryable(value = {Exception.class}, maxAttempts = 3, backoff = @Backoff(200))
  12. String retry(int code) throws Exception;
  13. /**
  14. * 当重试达到上限后还是失败,则作为异常回调方法
  15. * @param th
  16. * @param code
  17. * @return
  18. */
  19. @Recover
  20. String recover(Throwable th, int code);
  21. }

MyReretService实现类:

  1. @Slf4j
  2. @Service
  3. public class MyRetryServiceImpl implements MyRetryService {
  4. @Override
  5. public String retry(int code) throws Exception {
  6. log.info("请求retry接口");
  7. String result = HttpUtil.get("http://localhost:8080/http_test");
  8. if(code != 200){
  9. throw new Exception("接口请求异常");
  10. }
  11. return result;
  12. }
  13. @Override
  14. public String recover(Throwable th, int code) {
  15. log.error("回调方法执行!!!!");
  16. return "异常码为:" + code + ",异常信息:" + th.getMessage();
  17. }
  18. }

Controller:

  1. @Autowired
  2. private MyRetryService myRetryService;
  3. /**
  4. * 当请求code参数为200时,直接成功
  5. * 当code参数!=200时,会出发重试
  6. * @param code
  7. * @return
  8. * @throws Exception
  9. */
  10. @GetMapping("retry_demo_spring_retry")
  11. public String retry_demo_spring_retry(Integer code) throws Exception {
  12. return myRetryService.retry(code);
  13. }

访问地址:http://localhost:8080/retry_demo_spring_retry?code=123

查看结果:可以看到接口重试了3次,最后执行了@Recover方法最后的回调。

2.4、Resilience4j

Resilience4j是一个轻量级、易于使用的轻量级“容错”包。它受Neflix Hystrix启发但只有一个依赖(Vavr),而不像Hystrix很多很多的依赖。同时它是一个 Java 库,可以帮助我们构建弹性和容错的应用程序。Resilience4j在“容错”方面提供了各种模式:断路器(Circuit Breaker)、重试(Retry)、限时器(Time Limiter)、限流器(Rate Limiter)、隔板(BulkHead)。我们今天讨论的话题是重试,那么今天就来演示下Retry。

Github地址:GitHub - resilience4j/resilience4j: Resilience4j is a fault tolerance library designed for Java8 and functional programming

首先,添加相应依赖:

  1. <dependency>
  2. <groupId>io.github.resilience4j</groupId>
  3. <artifactId>resilience4j-spring-boot2</artifactId>
  4. <version>2.1.0</version>
  5. </dependency>

application.yml配置相关策略,配置官方文档:https://resilience4j.readme.io/docs/retry

  1. resilience4j:
  2. retry:
  3. instances:
  4. retry_demo:
  5. max-attempts: 3 # 重试的上限次数
  6. wait-duration: 1s # 重试的间隔时间,配置为1s

我们改造一下上面spring-retry的demo。

controller:

  1. @GetMapping("retry_demo_spring_retry")
  2. @Retry(name = "retry_demo", fallbackMethod = "recover")
  3. public String retry_demo_spring_retry(Integer code) throws Exception {
  4. return myRetryService.retry(code);
  5. }
  6. public String recover(Throwable th) {
  7. log.error("回调方法执行!!!!");
  8. return "异常信息:" + th.getMessage();
  9. }

myRetryService:

  1. @Override
  2. public String retry(int code) throws Exception {
  3. log.info("请求retry接口");
  4. String result = HttpUtil.get("http://localhost:8080/http_test");
  5. if(code != 200){
  6. throw new Exception("接口请求异常");
  7. }
  8. return result;
  9. }

程序执行,打印结果:

同样接口请求了3次,均失败后执行了fallback回调方法。

2.5、http请求网络工具内置重试方式

通常一些外部的http网络工具,都会内置一些重试的策略。如Apache HttpClient。这里以httpclient5为例。

首先添加依赖:

  1. <dependency>
  2. <groupId>org.apache.httpcomponents.client5</groupId>
  3. <artifactId>httpclient5</artifactId>
  4. <version>5.1.4</version>
  5. </dependency>

定义HttpClient相关类,指定重试策略。可以使用默认的DefaultHttpRequestRetryStrategy,也可以自定义重试策略CustomRetryStrategy。

  1. private static volatile CloseableHttpClient HTTP_CLIENT = null;
  2. static {
  3. if(HTTP_CLIENT == null){
  4. synchronized (HelloWorldController.class) {
  5. if(HTTP_CLIENT == null){
  6. HTTP_CLIENT = HttpClients.custom()
  7. // 设置重试策略
  8. // .setRetryStrategy(new DefaultHttpRequestRetryStrategy(3, TimeValue.NEG_ONE_SECOND))
  9. // 自定义重试策略
  10. .setRetryStrategy(new CustomRetryStrategy())
  11. .build();
  12. }
  13. }
  14. }
  15. }

CustomRetryStrategy:

  1. public static class CustomRetryStrategy implements HttpRequestRetryStrategy {
  2. @Override
  3. public boolean retryRequest(HttpRequest httpRequest, IOException e, int executeCount, HttpContext httpContext) {
  4. return false;
  5. }
  6. @Override
  7. public boolean retryRequest(HttpResponse httpResponse, int executeCount, HttpContext httpContext) {
  8. System.out.println("进入重试策略");
  9. if(executeCount > 3){
  10. System.out.println("重试超过3次,终止重试");
  11. return false;
  12. }
  13. if(httpResponse.getCode() != 200){
  14. System.out.println("http状态码不等于200,进行重试");
  15. return true;
  16. }
  17. // 其他情况,不重试
  18. return false;
  19. }
  20. @Override
  21. public TimeValue getRetryInterval(HttpResponse httpResponse, int executeCount, HttpContext httpContext) {
  22. return null;
  23. }
  24. }

Controller代码:

  1. @GetMapping("retry_demo_httpclient")
  2. public String retry_demo_httpclient(Integer code) throws Exception {
  3. return httpclientRetry(code);
  4. }
  5. private String httpclientRetry(int code) throws Exception {
  6. log.info("请求retry接口");
  7. // 这里模拟了一个不存在的地址
  8. HttpGet request = new HttpGet("http://localhost:8080/http_test1");
  9. CloseableHttpResponse httpResponse = HTTP_CLIENT.execute(request);
  10. String result = IoUtil.read(httpResponse.getEntity().getContent()).toString();
  11. if(code != 200){
  12. throw new Exception("接口请求异常");
  13. }
  14. return result;
  15. }

访问接口地址:http://localhost:8080/retry_demo_httpclient?code=200。查看控制台日志打印:

2.6、自定义重试工具

装X的话,我们还可以自定义我们的重试工具。其实无非以下几个步骤:

  1. 自定义重试的工具类
  2. 接收一个方法调用,并对该方法进行异常捕获
  3. 如果捕获了该异常,则进行一定间隔,然后重新请求
  4. 记录请求次数,如果超过上限,则提示异常信息

直接定义一个重试的工具类RetryUtil.java:

  1. import cn.hutool.core.thread.ThreadUtil;
  2. import lombok.extern.slf4j.Slf4j;
  3. import java.util.concurrent.TimeUnit;
  4. import java.util.concurrent.atomic.AtomicInteger;
  5. import java.util.function.Function;
  6. import java.util.function.Supplier;
  7. @Slf4j
  8. public class RetryUtil {
  9. /**
  10. * 重试方法
  11. * @param invokeFunc 原方法调用
  12. * @param maxAttempts 重试次数上限
  13. * @param deplay 重试的间隔时间
  14. * @param timeUnit 重试的间隔时间单位
  15. * @param faultFunc 如果超过重试上限次数,那么会执行该错误回调方法
  16. * @return
  17. * @param <T>
  18. */
  19. public static <T> T retry(Supplier<T> invokeFunc, int maxAttempts, long deplay, TimeUnit timeUnit, Function<Throwable, T> faultFunc) {
  20. AtomicInteger retryTimes = new AtomicInteger(0);
  21. for(;;) {
  22. try{
  23. return invokeFunc.get();
  24. } catch (Throwable th) {
  25. if(retryTimes.get() > maxAttempts){
  26. log.error("重试次数超过{}次,进入失败回调", retryTimes.get());
  27. return faultFunc.apply(th);
  28. }
  29. ThreadUtil.sleep(deplay, timeUnit);
  30. retryTimes.getAndAdd(1);
  31. }
  32. }
  33. }
  34. }

工具类使用:

  1. @GetMapping("retry_demo_custom")
  2. public String retry_demo_custom(Integer code) {
  3. return RetryUtil.retry(() -> {
  4. String result = null;
  5. try {
  6. result = customRetry(code);
  7. } catch (Exception e) {
  8. throw new RuntimeException(e);
  9. }
  10. return result;
  11. }, 3, 1000, TimeUnit.MILLISECONDS, Throwable::getMessage);
  12. }
  13. private String customRetry(int code) throws Exception {
  14. log.info("请求customRetry接口");
  15. String result = HttpUtil.get("http://localhost:8080/http_test");
  16. if(code != 200){
  17. throw new Exception("接口请求异常");
  18. }
  19. return result;
  20. }

执行完后,访问地址:http://localhost:8080/retry_demo_custom?code=2001

这里只是简单的进行了定义,如果项目中使用肯定需要考虑更复杂的因素。如进入重试时不一定只有异常的时候需要重试,可以指定重试策略,然后制定进入重试策略的规则。

2.7、并发框架异步重试

在 Java 并发框架中,异步重试通常涉及到使用线程池和定时器,以便在异步任务失败后进行重试。以下是一个简单的示例,演示了如何使用 CompletableFuture、ScheduledExecutorService 和 CompletableFuture.supplyAsync 来实现异步任务的重试。

  1. import java.util.concurrent.CompletableFuture;
  2. import java.util.concurrent.Executors;
  3. import java.util.concurrent.ScheduledExecutorService;
  4. import java.util.concurrent.TimeUnit;
  5. public class AsyncRetryExample {
  6. private static final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
  7. public static void main(String[] args) {
  8. // 示例异步任务,这里使用 supplyAsync,你可以根据实际情况选择其他异步任务
  9. CompletableFuture<String> asyncTask = CompletableFuture.supplyAsync(() -> performAsyncTask("Task"));
  10. // 异步任务失败后的重试逻辑
  11. retryAsyncTask(asyncTask, 3, 1, TimeUnit.SECONDS);
  12. }
  13. private static CompletableFuture<String> performAsyncTask(String taskName) {
  14. // 模拟异步任务,这里可以是任何异步操作
  15. System.out.println("Performing async task: " + taskName);
  16. // 这里模拟任务失败的情况
  17. throw new RuntimeException("Task failed");
  18. }
  19. private static <T> void retryAsyncTask(CompletableFuture<T> asyncTask, int maxRetries, long delay, TimeUnit timeUnit) {
  20. asyncTask.exceptionally(throwable -> {
  21. // 异步任务失败后的处理逻辑
  22. System.out.println("Task failed: " + throwable.getMessage());
  23. // 重试逻辑
  24. if (maxRetries > 0) {
  25. System.out.println("Retrying...");
  26. CompletableFuture<T> retryTask = CompletableFuture.supplyAsync(() -> performAsyncTask("Retry Task"));
  27. // 递归调用,进行重试
  28. retryAsyncTask(retryTask, maxRetries - 1, delay, timeUnit);
  29. } else {
  30. System.out.println("Max retries reached. Task failed.");
  31. }
  32. return null; // 必须返回 null,否则会影响链式调用
  33. });
  34. }
  35. }

示例中,performAsyncTask 模拟了一个异步任务,如果任务失败,它会抛出一个运行时异常。retryAsyncTask 方法用于处理异步任务的失败情况,并进行重试。在重试时,它使用 CompletableFuture.supplyAsync 创建一个新的异步任务,模拟了重试的过程。请注意,这只是一个简单的示例,实际应用中可能需要更复杂的重试策略和错误处理逻辑。

2.8、消息队列

网上还有一种消息队列的方式来实现,这里没过多的去研究过,目前以上几种方式应该也是够用的了。这里直接贴出网上的部分代码,使用 RabbitMQ 作为消息队列,演示了请求重试的实现:

首先添加依赖:

  1. <dependencies>
  2. <dependency>
  3. <groupId>com.rabbitmq</groupId>
  4. <artifactId>amqp-client</artifactId>
  5. <version>5.13.1</version>
  6. </dependency>
  7. </dependencies>

然后,创建一个发送者和接收者类:

消息发送者(Producer)

  1. import com.rabbitmq.client.Channel;
  2. import com.rabbitmq.client.Connection;
  3. import com.rabbitmq.client.ConnectionFactory;
  4. import java.io.IOException;
  5. import java.util.concurrent.TimeoutException;
  6. public class MessageProducer {
  7. private static final String QUEUE_NAME = "retry_queue";
  8. public static void main(String[] args) throws IOException, TimeoutException {
  9. ConnectionFactory factory = new ConnectionFactory();
  10. factory.setHost("localhost");
  11. try (Connection connection = factory.newConnection(); Channel channel = connection.createChannel()) {
  12. channel.queueDeclare(QUEUE_NAME, false, false, false, null);
  13. // 模拟发送请求
  14. String request = "Your request data";
  15. // 将请求发送到队列
  16. channel.basicPublish("", QUEUE_NAME, null, request.getBytes());
  17. System.out.println(" [x] Sent '" + request + "'");
  18. }
  19. }
  20. }

消息接收者(Consumer)

  1. import com.rabbitmq.client.*;
  2. import java.io.IOException;
  3. import java.util.concurrent.TimeoutException;
  4. public class MessageConsumer {
  5. private static final String QUEUE_NAME = "retry_queue";
  6. public static void main(String[] args) throws IOException, TimeoutException {
  7. ConnectionFactory factory = new ConnectionFactory();
  8. factory.setHost("localhost");
  9. try (Connection connection = factory.newConnection(); Channel channel = connection.createChannel()) {
  10. channel.queueDeclare(QUEUE_NAME, false, false, false, null);
  11. // 设置消息监听器
  12. DeliverCallback deliverCallback = (consumerTag, delivery) -> {
  13. String request = new String(delivery.getBody(), "UTF-8");
  14. // 模拟处理请求,这里可能会出现处理失败的情况
  15. boolean processingSucceeded = processRequest(request);
  16. if (processingSucceeded) {
  17. System.out.println(" [x] Received and processed: '" + request + "'");
  18. } else {
  19. // 处理失败,将请求重新放入队列,进行重试
  20. channel.basicPublish("", QUEUE_NAME, null, delivery.getBody());
  21. System.out.println(" [x] Processing failed. Retrying: '" + request + "'");
  22. }
  23. };
  24. // 消费消息
  25. channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> {
  26. });
  27. }
  28. }
  29. private static boolean processRequest(String request) {
  30. // 模拟处理请求的方法
  31. // 在实际应用中,这里应该是对请求的处理逻辑
  32. // 返回 true 表示处理成功,返回 false 表示处理失败,需要进行重试
  33. // 这里简单地模拟了一个失败的情况
  34. return !request.equals("Your request data");
  35. }
  36. }

示例中,消息发送者(MessageProducer)将请求发送到名为 "retry_queue" 的队列中。消息接收者(MessageConsumer)监听队列,当接收到消息时,模拟处理请求的逻辑。如果处理失败,将请求重新放入队列进行重试。

3、小结

接口请求重试机制对保证系统高可用非常关键,需要根据业务需求选择合适的重试策略。常用的组合策略包括带最大次数的定时/指数退避重试、故障转移重试等。重试机制需要综合设置以达到容错效果 又避免产生过大的系统负载。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/article/detail/39595
推荐阅读
相关标签
  

闽ICP备14008679号