1. Обзор
В этом руководстве мы рассмотрим несколько возможных способов реализации тайм-аутов запросов для Spring REST API.
Мы обсудим преимущества и недостатки каждого из них. Время ожидания запроса полезно для предотвращения плохого взаимодействия с пользователем, особенно если есть альтернатива, которую мы можем использовать по умолчанию, когда ресурс занимает слишком много времени. Этот шаблон проектирования называется шаблоном Circuit Breaker , но мы не будем подробно на нем останавливаться.
2. @Транзакционные тайм-
ауты
Один из способов, которым мы можем реализовать тайм-аут запроса для вызовов базы данных, — это воспользоваться аннотацией Spring @Transactional
. У него есть свойство тайм
-аута , которое мы можем установить. Значение по умолчанию для этого свойства равно -1, что эквивалентно полному отсутствию времени ожидания. Для внешней настройки значения тайм-аута вместо этого необходимо использовать другое свойство — timeoutString .
Например, предположим, что мы установили этот тайм-аут равным 30. Если время выполнения аннотированного метода превышает это количество секунд, будет выдано исключение. Это может быть полезно для отката длительных запросов к базе данных.
Чтобы увидеть это в действии, давайте напишем очень простой уровень репозитория JPA, который будет представлять внешнюю службу, выполнение которой занимает слишком много времени и вызывает тайм-аут. В этом расширении 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++;
}
}
}
}
Если мы вызовем наш метод WasteTime()
внутри транзакции с тайм-аутом в 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.");
}
Вызов этой конечной точки приводит к ошибке 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.");
}));
}
TimeLimiter предлагает несколько преимуществ по сравнению с решением
@Transactional
. А именно, он поддерживает точность до доли секунды и немедленное уведомление о тайм-ауте. Тем не менее, его по-прежнему необходимо вручную включать во все конечные точки, для которых требуется тайм-аут, он требует некоторого длинного кода-обертки, а ошибка, которую он создает, по-прежнему является общей ошибкой 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, когда мы определяем свойство приложения. Ответ возвращается сразу после достижения тайм-аута, и он даже возвращает более описательную ошибку HTTP 503 вместо общей 500. Кроме того, каждая конечная точка в нашем проекте автоматически наследует эту конфигурацию тайм-аута.
Давайте рассмотрим другой вариант, который позволит нам определять тайм-ауты с большей степенью детализации.
5. Тайм- ауты веб- клиента
Вместо того, чтобы устанавливать тайм-аут для всей конечной точки, возможно, мы хотим просто установить тайм-аут для одного внешнего вызова. WebClient
— это реактивный веб-клиент Spring, который позволяет нам настроить время ожидания ответа.
Также можно настроить тайм-ауты для более старого объекта 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>
Давайте определим WebClient
с тайм-аутом ответа 250 миллисекунд, который мы можем использовать для вызова самих себя через localhost в его базовом URL-адресе:
@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();
}
После вызова этой конечной точки мы видим, что мы получаем тайм-аут WebClient
в виде ответа об ошибке 500 HTTP. Мы также можем проверить журналы, чтобы увидеть тайм-аут @Transactional
нисходящего потока . Но, конечно, его тайм-аут был бы напечатан удаленно, если бы мы вызывали внешний сервис вместо локального хоста.
Может потребоваться настройка разных тайм-аутов запросов для разных серверных служб, и это возможно с помощью этого решения. Кроме того, издатели ответов Mono
или Flux
, возвращаемые WebClient
, содержат множество методов обработки ошибок для обработки общего ответа об ошибке тайм-аута.
6. Заключение
В этой статье мы только что рассмотрели несколько различных решений для реализации тайм-аута запроса. Есть несколько факторов, которые следует учитывать при принятии решения о том, какой из них использовать.
Если мы хотим установить тайм-аут для наших запросов к базе данных, мы можем использовать метод Spring @Transactional
и его свойство timeout .
Если мы пытаемся интегрироваться с более широким шаблоном прерывателя цепи, использование TimeLimiter Resilience4j имело
бы смысл. Использование свойства Spring MVC request-timeout
лучше всего подходит для установки глобального тайм-аута для всех запросов, но мы можем легко определить более детальные тайм-ауты для каждого ресурса с помощью WebClient
.
Для рабочего примера всех этих решений код готов и готов к запуску из коробки на GitHub .