1. 概述
在本文中,我们将介绍几种在 Spring REST API 中实现请求超时机制的方式,并分析每种方式的优劣。
请求超时主要用于避免用户长时间等待,特别是在调用外部资源时。如果某个请求耗时过长,我们可以采用 Circuit Breaker(熔断器)模式 来降级处理。但本文不深入讨论熔断机制,仅聚焦于如何设置请求超时。
2. 使用 @Transactional 设置超时
一种常见的做法是利用 Spring 的 @Transactional
注解来控制数据库操作的超时时间。它提供了一个 timeout
属性,默认值为 -1,表示永不超时。
示例代码如下:
@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.");
}
我们模拟了一个耗时操作 wasteTime()
:
public interface BookRepository extends JpaRepository<Book, String> {
default int wasteTime() {
Stopwatch watch = Stopwatch.createStarted();
// delay for 2 seconds
while (watch.elapsed(SECONDS) < 2) {
int i = Integer.MIN_VALUE;
while (i < Integer.MAX_VALUE) {
i++;
}
}
}
}
当方法执行超过 1 秒时,会抛出异常,事务回滚。
✅ 优点:
- 实现简单
- 适用于数据库事务场景
❌ 缺点:
- 仅限于数据库操作
- 需要在每个方法上手动添加注解
- 超时后仍需等待完整执行时间,无法立即中断
3. 使用 Resilience4j 的 TimeLimiter
Resilience4j 是一个专注于远程调用容错的库,其中的 TimeLimiter
模块非常适合处理请求超时问题。
首先引入依赖:
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-timelimiter</artifactId>
<version>2.1.0</version>
</dependency>
然后定义一个 500ms 的超时器:
private TimeLimiter ourTimeLimiter = TimeLimiter.of(TimeLimiterConfig.custom()
.timeoutDuration(Duration.ofMillis(500)).build());
使用方式如下:
@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.");
}));
}
✅ 优点:
- 支持毫秒级精度
- 可以与熔断器配合使用
- 超时响应更快
❌ 缺点:
- 仍需手动包装每个接口
- 返回类型必须是
Callable
- 错误依然是 500 状态码
4. 使用 Spring MVC 的 request-timeout
Spring 提供了全局配置项 spring.mvc.async.request-timeout
,可以设置异步请求的超时时间。
配置示例:
spring.mvc.async.request-timeout=750
接口代码如下:
@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.");
};
}
✅ 优点:
- 全局生效
- 配置简单
- 超时返回 503 状态码,语义更清晰
❌ 缺点:
- 仅对返回
Callable
的接口生效 - 不支持细粒度控制
5. 在 HTTP 客户端中设置超时
有时候我们只想对某个外部调用设置超时,而不是整个接口。此时可以使用 HTTP 客户端如 WebClient
或 RestClient
进行控制。
5.1. WebClient 超时设置
首先引入 WebFlux 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<version>2.4.2</version>
</dependency>
定义 WebClient 并设置响应超时:
@Bean
public WebClient webClient() {
return WebClient.builder()
.baseUrl("http://localhost:8080")
.clientConnector(new ReactorClientHttpConnector(
HttpClient.create().responseTimeout(Duration.ofMillis(250))
))
.build();
}
使用 WebClient 调用其他接口:
@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();
}
✅ 优点:
- 可以为不同服务设置不同超时
- 支持响应式编程模型
- 提供丰富的错误处理机制
❌ 缺点:
- 需要引入 WebFlux
- 语法相对复杂
5.2. RestClient 超时设置
Spring Framework 6 引入了 RestClient
,作为 WebClient
的同步替代方案。
引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
创建 RestClient Bean:
@Bean
public RestClient restClient() {
return RestClient.builder()
.baseUrl("http://localhost:" + serverPort)
.requestFactory(customRequestFactory())
.build();
}
ClientHttpRequestFactory customRequestFactory() {
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.DEFAULTS
.withConnectTimeout(Duration.ofMillis(200))
.withReadTimeout(Duration.ofMillis(200));
return ClientHttpRequestFactories.get(settings);
}
发起请求:
@GetMapping("/author/restclient")
public String getWithRestClient(@RequestParam String title) {
return restClient.get()
.uri(uriBuilder -> uriBuilder
.path("/author/transactional")
.queryParam("title", title)
.build())
.retrieve()
.body(String.class);
}
✅ 优点:
- 同步调用,使用简单
- API 设计现代化
- 易于与现有同步代码集成
6. 总结
方案 | 适用场景 | 是否全局 | 是否支持毫秒级 | 是否需要额外包装 |
---|---|---|---|---|
@Transactional | 数据库事务 | ❌ | ✅ | ❌ |
Resilience4j TimeLimiter | 熔断/超时控制 | ❌ | ✅ | ✅ |
spring.mvc.async.request-timeout | 全局异步请求超时 | ✅ | ✅ | ❌ |
WebClient | 外部调用超时 | ❌ | ✅ | ✅ |
RestClient | 同步外部调用超时 | ❌ | ✅ | ✅ |
选择哪种方案取决于你的具体需求:
- 如果是数据库操作,使用
@Transactional
- 如果配合熔断机制,推荐
TimeLimiter
- 全局统一超时控制,使用
spring.mvc.async.request-timeout
- 细粒度控制外部调用,使用
WebClient
或RestClient
每种方案都有其适用场景,合理选用可以有效提升系统的健壮性和用户体验。