Spring Boot 的重试(Retry)和恢复(Recover)功能-Spring专区论坛-技术-SpringForAll社区

Spring Boot 的重试(Retry)和恢复(Recover)功能

在您的项目中,您负责通过 POST 请求将数据发送到第三方 API。您的代码一直运行良好,直到有一天,第三方端点遭遇故障,导致您发送的大量消息未能成功传递,而是被系统无情地丢弃。面对企业迫切的需求,他们明确表示不希望丢失任何数据,您需要迅速找到解决方案。

本文将详细探讨如何应对这一挑战,确保在第三方端点不稳定的情况下,数据传输的可靠性和持久性得以维持。我们将介绍一系列策略和技术,包括改进错误处理机制、实施重试逻辑、以及采用消息队列等方法,以确保即使在面对端点故障时,数据也能安全地存储和后续处理。通过这些措施,您将能够构建一个更为健壮和可靠的系统,满足企业的关键需求。

项目构建

pom.xml 配置

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelVersion>4.0.0</modelVersion>
 <parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>3.2.4</version>
  <relativePath /> <!-- lookup parent from repository -->
 </parent>
 <groupId>com.failed-retry-demo.app</groupId>
 <artifactId>FailedRetryDemo</artifactId>
 <version>0.0.1-SNAPSHOT</version>
 <name>FailedRetryDemo</name>
 <description>Failed Retry Demo Application</description>
 <properties>
  <java.version>17</java.version>
 </properties>
 <dependencies>
  <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-jpa</artifactId>
  </dependency>
  <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
   <groupId>org.springframework.retry</groupId>
   <artifactId>spring-retry</artifactId>
  </dependency>
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-aspects</artifactId>
  </dependency>
  <dependency>
   <groupId>com.h2database</groupId>
   <artifactId>h2</artifactId>
   <scope>runtime</scope>
  </dependency>
  <dependency>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok</artifactId>
   <optional>true</optional>
  </dependency>
  <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <scope>test</scope>
  </dependency>
 </dependencies>

 <build>
  <plugins>
   <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
     <excludes>
      <exclude>
       <groupId>org.projectlombok</groupId>
       <artifactId>lombok</artifactId>
      </exclude>
     </excludes>
    </configuration>
   </plugin>
  </plugins>
 </build>

</project>

主类

我们需要在配置之上使用@EnableRetry和@EnableScheduling,如下所示:

@SpringBootApplication
@EnableRetry
@EnableScheduling
public class FailedRetryDemoApplication {

   public static void main(String[] args) {
       SpringApplication.run(FailedRetryDemoApplication.class, args);
   }

}

Article DTO

@Data
@AllArgsConstructor
@ToString
@NoArgsConstructor
public class Article {

    int id;
    String name;

}

Article Entity

@Entity
@Data
@AllArgsConstructor
@ToString
@NoArgsConstructor
public class ArticleEntity {

   @Id
   int id;
   String name;

}

ArticleEntity 和 Article 对象非常相似。但他们有本质区别,一个是Entity,一个是DTO。这样的设计是为了在通过网络与其他 API 进行数据传输时使用 ArticleDTO,而在需要将数据保存到数据库或从数据库中获取数据时使用 ArticleEntity。

ArticleRepository

public interface ArticleRepository extends JpaRepository<ArticleEntity,Integer>{

 Optional<ArticleEntity> findByName(String name);

}

RestController

@RestController
@AllArgsConstructor
public class DemoRestController {
 
 private final DemoService dservice;
 private final Integer PORT = 11000; 

 @PostMapping("/create-post")
 public void sendToApi(@RequestBody Article articleDto) throws Exception {
  dservice.sendToApi(articleDto,PORT);
 }
 
}

Service

@Service
@RequiredArgsConstructor
public class DemoService {

 private final RestClient.Builder restClient = RestClient.builder();
 private final ArticleRepository repo;

 @Retryable(maxAttempts = 5, backoff = @Backoff(delay = (3000),multiplier = 2, maxDelay = 10000), recover = "storeInDb")  
 public void sendToApi(Article articleDto, Integer PORT) {
  System.out.println("Retry Attempt at "+" "+LocalDateTime.now());
  restClient
  .build()
  .post()
  .uri("http://localhost:"+PORT+"/create-article")
  .body(articleDto)
  .retrieve()
  .onStatus(status -> status.isError(), (request, response)->{
   System.out.println("Error occured!!!");
  })
  .onStatus(status -> !status.isError(), (req,res)->{
   repo.deleteById(articleDto.getId());
   System.out.println("SUCCESS");
  })
  .toBodilessEntity();

 }
 
 @Recover
 public void storeInDb(Article articleDto, Integer PORT) {
  System.out.println("Saving Failed Data into DB");
  ArticleEntity entity = new ArticleEntity(articleDto.getId(),articleDto.getName());
  if(repo.findByName(articleDto.getName()).isEmpty()) {
      repo.save(entity);
  }
  System.out.println("RECOVERD "+articleDto.toString());
 }
 
 @Scheduled(cron = "0 0 * * * ?")
 public void sendFailedDataToApi() {
  send(0,5000);
 }

 public void send(int pageNo, int pageSize) {
  Page<ArticleEntity> page = repo.findAll(PageRequest.of(pageNo, pageSize));
  while(page.getSize() > 0) {
   page.stream().map(ele -> new Article(ele.getId(),ele.getName())).forEach(ele -> {
     this.sendToApi(ele, 11000);
   });
   page = repo.findAll(PageRequest.of(pageNo+1, pageSize));
  }
 }

}

在 Service 中有 4 个方法:

sendToApi :通过应用 @Retryable 注解,我们确保了在未能从第三方端点接收到响应时,相关方法将被自动重试最多 5 次。此外,通过集成 BackOff 机制,我们实现了在每次重试之间引入一个短暂的等待期,以优化请求的效率和减少对端点的压力。如果在经过所有重试尝试后仍未能获得响应,系统将自动转而调用带有 @Recover 注解的方法,以处理这种异常情况。

storeInDb: @Recover 方法的设计与 sendToApi 方法保持参数一致性,以便在重试机制失效后,能够无缝地接管处理流程。在该方法的实现中,我们将未能成功发送的数据记录到专门的发送失败数据库中,以便后续进行进一步的分析或手动重试,从而确保数据的完整性和系统的可靠性。

sendFailedDataToApi 和 send:该函数的核心功能在于从数据库表中提取数据,并通过反复尝试将其发送到指定的 API 端点。一旦我们收到成功的状态码,表明数据已成功传输,sendToApi 函数将负责从表中移除该记录,以保持数据的一致性。然而,如果从第三方接收到错误响应,系统将仅简单地记录“Error Occured”,以便于后续的故障排查。更为关键的是,面对网络中断等不可预见的情况,我们的重试与恢复机制将自动启动,确保数据传输的持续性和可靠性。

测试

用你喜欢的HTTP请求工具发送请求:

d2b5ca33bd20240807220847

存储失败数据的表

d2b5ca33bd20240807221226

日志:

d2b5ca33bd20240807221217

一小时后,当第三方 API 启动时,数据被发送出去,并从我们的表中删除。我故意不运行第三方端点来显示重试功能。

d2b5ca33bd20240807221259

第三方API只不过是一个可以管理Article的CRUD API。这是非常简单且基本的 Rest Api。

在本文中,我们看到了@Retry/@Recover 功能。感谢您阅读本文。

 

请登录后发表评论

    没有回复内容