Что такое паттерны Retry и Timeout?
Retry и Timeout — базовые паттерны отказоустойчивости при межсервисном взаимодействии. Timeout ограничивает время ожидания ответа, Retry автоматически повторяет запрос при transient-ошибках.
Timeout
Без таймаута один зависший сервис может «повесить» всю цепочку вызовов.
Пример
// Таймаут через WebClient
WebClient webClient = WebClient.builder()
.baseUrl("http://customer-service")
.clientConnector(new ReactorClientHttpConnector(
HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000)
.responseTimeout(Duration.ofSeconds(5))
))
.build();
Retry
Автоматический повтор запроса при сбое. Повторять только идемпотентные операции и только при transient (временных) ошибках.
Resilience4j Retry
@Service
public class CustomerService {
@Retry(name = "customerService", fallbackMethod = "getCustomerFallback")
public CustomerDto getCustomer(Long id) {
return customerClient.getCustomer(id);
}
private CustomerDto getCustomerFallback(Long id, Exception e) {
log.warn("Все попытки вызова customer-service исчерпаны: {}", e.getMessage());
return cachedCustomerRepository.findById(id)
.orElseThrow(() -> new ServiceUnavailableException("Customer service недоступен"));
}
}
resilience4j:
retry:
instances:
customerService:
max-attempts: 3
wait-duration: 500ms
exponential-backoff-multiplier: 2 # 500ms, 1s, 2s
retry-exceptions:
- java.io.IOException
- java.net.SocketTimeoutException
- org.springframework.web.client.HttpServerErrorException
ignore-exceptions:
- org.springframework.web.client.HttpClientErrorException # 4xx — не повторять!
Exponential Backoff с Jitter
Пример
Попытка 1: ошибка → ждём 500ms + random(0-200ms)
Попытка 2: ошибка → ждём 1000ms + random(0-400ms)
Попытка 3: ошибка → ждём 2000ms + random(0-800ms)
Попытка 4: ошибка → fallback
Без jitter все клиенты, получившие ошибку одновременно, будут повторять запросы в одно и то же время, создавая пиковую нагрузку («грозовое стадо»).
Spring Retry
@Configuration
@EnableRetry
public class RetryConfig {}
@Service
public class PaymentGatewayService {
@Retryable(
retryFor = {SocketTimeoutException.class, HttpServerErrorException.class},
noRetryFor = {HttpClientErrorException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 1000, multiplier = 2, maxDelay = 10000)
)
public PaymentResult sendPayment(PaymentRequest request) {
return paymentGateway.execute(request);
}
@Recover
public PaymentResult recoverSendPayment(Exception e, PaymentRequest request) {
log.error("Платёжный шлюз недоступен после всех попыток: {}", e.getMessage());
return PaymentResult.deferred(request.getId());
}
}
Комбинация паттернов (порядок имеет значение!)
Пример
Запрос → Retry → CircuitBreaker → Timeout → Вызов сервиса
Пример
resilience4j:
retry:
instances:
paymentGateway:
max-attempts: 3
wait-duration: 500ms
circuitbreaker:
instances:
paymentGateway:
failure-rate-threshold: 50
wait-duration-in-open-state: 30s
timelimiter:
instances:
paymentGateway:
timeout-duration: 5s
На собеседовании: обязательно упомяните exponential backoff с jitter — это показывает понимание реальных проблем. Ключевое правило: retry только для идемпотентных операций и только для 5xx, не для 4xx. Частая ошибка — ретраить бизнес-ошибки (400 Bad Request).