给 Spring REST API 设置请求超时
1、概览
本文将带你了解给 Spring REST API 设置请求超时的几种方法。
当资源耗时过长时,请求超时机制可以避免糟糕的用户体验。当然也可以使用断路器模式(Circuit Breaker pattern)来实现,本文不细说。
2、@Transactional
超时
在数据库调用中实现请求超时的一种方法是利用 Spring 的 @Transactional
注解。它有一个 timeout
属性可以设置。该属性的默认值是 -1
,相当于没有任何超时。
例如,假设将超时设置为 30 秒。如果注解方法的执行时间超过这个秒数,就会抛出异常。这对于回滚长时间运行的数据库查询可能很有用。
编写一个非常简单的 JPA Repository,它代表一个外部服务,该服务需要太长时间才能完成并导致超时。
这个 Repository
中有一个耗时的方法:
public interface BookRepository extends JpaRepository<Book, String> {
default int wasteTime() {
Stopwatch watch = Stopwatch.createStarted();
// 延迟 2 秒
while (watch.elapsed(SECONDS) < 2) {
int i = Integer.MIN_VALUE;
while (i < Integer.MAX_VALUE) {
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>2.1.0</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
功能的一个子集,可与断路器模式完美对接。
4、Spring MVC request-timeout
Spring 提供了一个名为 spring.mvc.async.request-timeout
的属性。该属性允许以毫秒为精度定义请求超时。
定义该属性,设置超时时间为 750 毫秒:
spring.mvc.async.request-timeout=750
该属性是全局属性,可从外部配置,但与 TimeLimiter
解决方案一样,它只适用于返回 Callable
的端点。
定义一个与 TimeLimiter
示例类似的端点,但无需将逻辑封装在 Future
中,也无需提供 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 会在定义 application properties 时自动实现配置。一旦超时,响应将立即返回,甚至会返回一个描述性更强的 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
,可以用它通过 baseURL 中的 localhost
调用自己:
@Bean
public WebClient webClient() {
return WebClient.builder()
.baseUrl("http://localhost:8080")
.clientConnector(new ReactorClientHttpConnector(
HttpClient.create().responseTimeout(Duration.ofMillis(250))
))
.build();
}
显然,这可以很容易地从外部配置这个超时值以及 base URL 和其他的几个可选属性。
现在,将 WebClient
注入 Controller,并用它来调用自己的 /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
的超时,但如果调用的是外部服务而不是 localhost
,它的超时信息会在远程服务中打印出来。
为不同的后端服务配置不同的请求超时可能是必要的,本解决方案也可以做到这一点。此外,WebClient
返回的 Mono
或 Flux
响应包含大量错误处理方法,可用于处理通用超时错误响应。
6、总结
本文介绍了在 Spring REST API 中实现请求超时的几种不同解决方案。
如果想对数据库请求设置超时,可能需要使用 Spring 的 @Transactional
方法及其 timeout
属性。如果想与更广泛的断路器模式集成,则使用 Resilience4j 的 TimeLimiter
会更合理。使用 Spring MVC 的 request-timeout
属性最适合为所有请求设置全局超时,也可以使用 WebClient
以自请求的方式为每个资源轻松定义更细粒度的超时。
Ref:https://www.baeldung.com/spring-rest-timeout