为Spring REST API设置请求超时

2023/05/19

1. 概述

在本教程中,我们将探索几种可能的方法来实现Spring REST API的请求超时。

然后我们将讨论每种方法的优点和缺点。请求超时对于防止糟糕的用户体验很有用,特别是如果有一个替代方案我们可以默认为资源花费太长时间。这种设计模式称为断路器模式,这里不再赘述。

2. @Transactional超时

我们可以在数据库调用上实现请求超时的一种方法是利用Spring的@Transactional注解。它有一个我们可以设置的超时属性。此属性的默认值为-1,相当于根本没有任何超时。对于超时值的外部配置,我们必须改用不同的属性timeoutString。

例如,假设我们将这个超时设置为30。如果被注解的方法执行时间超过这个秒数,就会抛出异常。这对于回滚长时间运行的数据库查询可能很有用。

为查看实际效果,我们将编写一个非常简单的JPA Repository层,该层将表示完成时间过长并导致超时的外部服务。这个JpaRepository扩展中有一个耗时的方法:

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秒的事务中调用我们的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时间限制器

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>而不是原始字符串。

TimeLimiter仅包含Resilience4j的一部分功能,并与断路器模式很好地交互。

4. Spring MVC请求超时

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响应包含大量错误处理方法,用于处理一般超时错误响应。

6. 总结

在本文中,我们探索了几种实现请求超时的不同解决方案。在决定使用哪一个时,有几个因素需要考虑。

如果我们想对我们的数据库请求设置超时,我们可能需要使用Spring的@Transactional方法及其超时属性。如果我们试图与更广泛的断路器模式集成,使用Resilience4j的TimeLimiter会很有意义。使用Spring MVC请求超时属性最适合为所有请求设置全局超时,但我们也可以使用WebClient轻松地为每个资源定义更细粒度的超时。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章