在您的项目中,您负责通过 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请求工具发送请求:
存储失败数据的表
日志:
一小时后,当第三方 API 启动时,数据被发送出去,并从我们的表中删除。我故意不运行第三方端点来显示重试功能。
第三方API只不过是一个可以管理Article的CRUD API。这是非常简单且基本的 Rest Api。
在本文中,我们看到了@Retry/@Recover 功能。感谢您阅读本文。
没有回复内容