1.概述
在本教程中,我们将探讨几种可能的方法来实现Spring REST API的请求超时。
我们将讨论每种方法的优缺点。请求超时对于防止不良的用户体验很有用,尤其是在存在资源占用时间过长的情况下,我们可以默认使用其他方法时。这种设计模式称为“断路器”模式,但在此不再赘述。
2. @ @Transactional
超时
我们可以对数据库调用实现请求超时的一种方法是利用Spring的@Transactional
注释。它具有我们可以设置的timeout
属性。此属性的默认值为-1,这等效于根本没有任何超时。对于超时值的外部配置,必须使用另一个属性timeoutString
代替。
例如,假设我们将此超时设置为30。如果带注释的方法的执行时间超过了此秒数,则将引发异常。这对于回滚长时间运行的数据库查询可能很有用。
为了了解这一点,让我们编写一个非常简单的JPA存储库层,该层将代表一个外部服务,该外部服务需要很长时间才能完成,并且会导致超时。此JpaRepository扩展中有一个耗时的方法:
public interface BookRepository extends JpaRepository<Book, String> {
default int wasteTime() {
int i = Integer.MIN_VALUE;
while(i < Integer.MAX_VALUE) {
i++;
}
return i;
}
}
如果我们在事务内部以1秒的超时时间调用wasteTime()
方法,则超时将在该方法完成执行之前过去:
@GetMapping("/author/transactional")
@Transactional(timeout = 1)
public String getWithTransactionTimeout(@RequestParam String title) {
bookRepository.wasteTime();
return bookRepository.findById(title)
.map(Book::getAuthor)
.orElse("No book found for this title.");
}
调用此端点会导致500 HTTP错误,我们可以将其转换为更有意义的响应。它还只需很少的设置即可实施。
但是,此超时解决方案有一些缺点。
首先,它依赖于具有Spring管理的事务的数据库。它也不是全局适用于项目的,因为注释必须出现在需要它的每个方法或类上。它还不允许亚秒精度。最后,在达到超时时,它不会缩短请求的时间,因此,请求实体仍然必须等待完整的时间。
让我们考虑其他一些选择。
3. Resilience4j TimeLimiter
Resilience4j是一个库,主要用于管理远程通信的容错能力。我们在这里感兴趣的是它的TimeLimiter
模块。
首先,我们必须在项目中包含resilience4j-timelimiter
依赖项:
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-timelimiter</artifactId>
<version>1.6.1</version>
</dependency>
接下来,让我们定义一个简单的TimeLimiter
,其超时时间为500毫秒:
private TimeLimiter ourTimeLimiter = TimeLimiter.of(TimeLimiterConfig.custom()
.timeoutDuration(Duration.ofMillis(500)).build());
这可以很容易地在外部配置。
我们可以使用TimeLimiter
来包装与@Transactional
示例所使用的逻辑相同的逻辑:
@GetMapping("/author/resilience4j")
public Callable<String> getWithResilience4jTimeLimiter(@RequestParam String title) {
return TimeLimiter.decorateFutureSupplier(ourTimeLimiter, () ->
CompletableFuture.supplyAsync(() -> {
bookRepository.wasteTime();
return bookRepository.findById(title)
.map(Book::getAuthor)
.orElse("No book found for this title.");
}));
}
与@Transactional
解决方案相比, TimeLimiter
许多优点。即,它支持亚秒精度和超时响应的立即通知。但是,它仍然必须手动包含在所有需要超时的端点中,它需要一些冗长的包装代码,并且它产生的错误仍然是通用的500 HTTP错误。另外,它需要返回Callable<String>
而不是原始String.
TimeLimiter
仅包含Resilience4j的功能的子集,并且与Circuit Breaker模式很好地接口。
4. Spring MVC request-timeout
Spring为我们提供了一个名为spring.mvc.async.request-timeout
的属性。此属性使我们可以定义毫秒级的请求超时。
让我们以750毫秒的超时时间定义属性:
spring.mvc.async.request-timeout=750
该属性是全局的,并且可以在外部配置,但是与TimeLimiter
解决方案一样,它仅适用于返回Callable
端点。让我们定义一个类似于TimeLimiter
示例的端点,但是不需要将逻辑包装在Futures
或提供TimeLimiter
:
@GetMapping("/author/mvc-request-timeout")
public Callable<String> getWithMvcRequestTimeout(@RequestParam String title) {
return () -> {
bookRepository.wasteTime();
return bookRepository.findById(title)
.map(Book::getAuthor)
.orElse("No book found for this title.");
};
}
我们可以看到代码不太冗长,并且在定义应用程序属性时,Spring会自动实现配置。一旦达到超时,响应将立即返回,它甚至返回更具描述性的503 HTTP错误,而不是通用的500错误。此外,我们项目中的每个端点都将自动继承此超时配置。
让我们考虑另一个选项,该选项使我们可以更详细地定义超时。
5. WebClient
超时
与其为整个端点设置超时,不如我们只想为单个外部呼叫设置超时。 WebClient
是Spring的反应式Web客户端,允许我们配置响应超时。
也可以在Spring较旧的RestTemplate
对像上配置超时。但是,大多数开发人员现在更喜欢WebClient
不是RestTemplate
。
要使用WebClient,我们必须首先将Spring的WebFlux依赖项添加到我们的项目中:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<version>2.4.2</version>
</dependency>
让我们定义一个响应时间为250毫秒的WebClient
,我们可以使用它在其基本URL中通过localhost进行调用:
@Bean
public WebClient webClient() {
return WebClient.builder()
.baseUrl("http://localhost:8080")
.clientConnector(new ReactorClientHttpConnector(
HttpClient.create().responseTimeout(Duration.ofMillis(250))
))
.build();
}
显然,我们可以在外部轻松配置此超时值。我们还可以在外部配置基本URL以及其他几个可选属性。
现在,我们可以将WebClient
注入到控制器中,并使用它来调用自己的/transactional
端点,该端点的超时时间仍为1秒。因为我们将WebClient
配置为在250毫秒内超时,所以我们应该看到它失败的速度比1秒快得多。
这是我们的新端点:
@GetMapping("/author/webclient")
public String getWithWebClient(@RequestParam String title) {
return webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/author/transactional")
.queryParam("title", title)
.build())
.retrieve()
.bodyToMono(String.class)
.block();
}
调用此终结点后,我们看到确实以500 HTTP错误响应的形式收到WebClient
的超时。我们还可以检查日志以查看下游@Transactional
超时。但是,当然,如果我们调用外部服务而不是本地主机,则它的超时将被远程打印。
为不同的后端服务配置不同的请求超时可能是必需的,并且使用此解决方案是可能的。此外, WebClient
返回的Mono
或Flux
响应发布者包含许多错误处理方法,用于处理通用超时错误响应。
六,结论
在本文中,我们刚刚探讨了几种用于实现请求超时的解决方案。在决定使用哪一个时,要考虑几个因素。
如果我们想对数据库请求设置超时,则可能要使用Spring的@Transactional
方法及其timeout
属性。如果我们试图与更广泛的断路器模式集成,则使用Resilience4j的TimeLimiter
会很有意义。使用Spring MVC request-timeout
属性最适合为所有请求设置全局超时,但是我们可以使用WebClient
轻松为每个资源定义更精细的超时。
0 评论