Мы разобрались с теорией Retry и Timeout: узнали, когда их использовать, как они помогают справляться с временными сбоями и почему они так важны в микросервисной архитектуре. Сегодня закатываем рукава и пишем код! Мы реализуем все эти механизмы с помощью Spring Boot и библиотеки Resilience4j.
Подготовка окружения
Начнём с самого важного — с подготовки нашего проекта. Как говорится, хороший повар перед готовкой раскладывает все ингредиенты на столе. Мы поступим так же!
Если вы используете Maven, добавьте в ваш pom.xml:
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot2</artifactId>
<version>1.7.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
А если вы в команде Gradle, то в build.gradle:
implementation 'io.github.resilience4j:resilience4j-spring-boot2:1.7.1'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-aop'
Настройка конфигурации
Теперь настроим наши механизмы защиты. Это как настройка автопилота: важно задать правильные параметры, чтобы он не дёргался при каждой кочке, но и не игнорировал серьёзные препятствия.
Создаём файл application.yml:
resilience4j:
retry:
instances:
myRetryInstance:
maxAttempts: 3
waitDuration: 1s
enableExponentialBackoff: true
exponentialBackoffMultiplier: 2
retryExceptions:
- org.springframework.web.client.ResourceAccessException
- java.util.concurrent.TimeoutException
timelimiter:
instances:
myTimeoutInstance:
timeoutDuration: 2s
cancelRunningFuture: true
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
Разберём настройки подробнее:
- Настройки Retry:
- maxAttempts: 3 — пробуем максимум 3 раза, потом сдаёмся
- waitDuration: 1s — между попытками ждём секунду
- enableExponentialBackoff: true — каждая следующая попытка ждёт дольше
- exponentialBackoffMultiplier: 2 — время ожидания увеличивается вдвое
- Настройки Timeout:
- timeoutDuration: 2s — две секунды на ответ, дольше не ждём
- cancelRunningFuture: true — если время вышло, отменяем операцию
Создание сервиса с Retry и Timeout
Теперь создадим сервис, который будет использовать наши настройки. Представим, что мы делаем клиент для работы с внешним API платёжной системы:
@Service
public class PaymentService {
private final RestTemplate restTemplate;
private static final Logger logger = LoggerFactory.getLogger(PaymentService.class);
public PaymentService(RestTemplateBuilder restTemplateBuilder) {
this.restTemplate = restTemplateBuilder.build();
}
@Retry(name = "myRetryInstance")
@TimeLimiter(name = "myTimeoutInstance")
public CompletableFuture<String> processPayment(String orderId) {
return CompletableFuture.supplyAsync(() -> {
logger.info("Попытка обработать платёж для заказа: {}", orderId);
return restTemplate.postForObject(
"http://payment-service/api/payments",
orderId,
String.class
);
});
}
// Fallback метод, который будет вызван, если все попытки не удались
public CompletableFuture<String> fallbackPayment(String orderId, Exception ex) {
logger.error("Все попытки платежа для заказа {} не удались. Причина: {}",
orderId, ex.getMessage());
return CompletableFuture.completedFuture(
"Платёж временно невозможен. Попробуйте позже."
);
}
}
Создадим контроллер для нашего сервиса:
@RestController
@RequestMapping("/api/payments")
public class PaymentController {
private final PaymentService paymentService;
public PaymentController(PaymentService paymentService) {
this.paymentService = paymentService;
}
@PostMapping("/process")
public CompletableFuture<String> processPayment(@RequestParam String orderId) {
return paymentService.processPayment(orderId);
}
}
Тестирование нашего решения
Для проверки работоспособности напишем тест:
@SpringBootTest
class PaymentServiceTest {
@Autowired
private PaymentService paymentService;
@Test
void testRetryAndTimeout() {
CompletableFuture<String> result = paymentService.processPayment("test-order-123");
assertThat(result)
.isCompletedWithin(Duration.ofSeconds(10))
.satisfies(response -> {
assertThat(response)
.isNotNull()
.containsIgnoringCase("платёж");
});
}
}
Мониторинг работы Retry и Timeout
Благодаря Actuator мы можем следить за работой наших механизмов защиты. Откройте в браузере:
- http://localhost:8080/actuator/retries
- http://localhost:8080/actuator/timelimiterss
Вы увидите метрики по количеству успешных и неудачных попыток, среднему времени ответа и многое другое.
Типичные ошибки и как их избежать
- Слишком короткий таймаут Если ваш timeout меньше, чем среднее время ответа сервиса — вы получите много ложных срабатываний.
- Слишком частые повторы Маленький waitDuration может создать дополнительную нагрузку на и так проблемный сервис.
- Забытый fallback Всегда определяйте fallback метод — это ваша страховка на случай, когда все попытки исчерпаны.
Домашнее задание
- Добавьте Retry и Timeout в свой проект
- Поэкспериментируйте с разными настройками
- Реализуйте умный fallback, который будет возвращать кэшированные данные
- Настройте мониторинг через Actuator
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ