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

哥们看看码农 2024-08-08 21:12:42

在您的项目中,您负责通过 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@EnableSchedulingpublic FailedRetryDemoApplication { public static void main(String[] args) { SpringApplication.run(FailedRetryDemoApplication.class, args); }}

Article DTO

@Data@AllArgsConstructor@ToString@NoArgsConstructorpublic Article { int id; String name;}

Article Entity

@Entity@Data@AllArgsConstructor@ToString@NoArgsConstructorpublic 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@AllArgsConstructorpublic 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@RequiredArgsConstructorpublic 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请求工具发送请求:

存储失败数据的表

日志:

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

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

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

来源:https://spring4all.com/forum-post/7456.html

0 阅读:0

哥们看看码农

简介:感谢大家的关注