12. Ускорить BFF параллельными вызовами
Условие задачи:
BFF обращается к двум внешним сервисам:
cart-serviceотвечает примерно за 0.7 секунды;recommendations-serviceотвечает примерно за 0.5 секунды.
При последовательном выполнении запрос занимает около 1.2 секунды. Необходимо выполнять вызовы параллельно и возвращать ответ менее чем за одну секунду.
Код:
@FeignClient(
name = "cart-service",
url = "http://shopping-cart"
)
public interface ShoppingCartServiceClient {
@GetMapping("/api/v1/shopping-cart")
CartDto getClientCart();
}
@FeignClient(
name = "recommendations-service",
url = "http://recommendations"
)
public interface RecommendationsServiceClient {
@GetMapping("/api/v1/recommendations")
List<RecommendationDto> getClientRecommendations();
}
public record ClientCartScreenDto(
CartDto cart,
List<RecommendationDto> recommendations
) {
}
@Service
public class ClientCartScreenService {
public ClientCartScreenDto getScreenData() {
// код тут
}
}
Спойлеры к решению
Подсказки
💡 Для параллельного выполнения можно использовать
CompletableFuture.💡 Для блокирующих Feign-вызовов используй отдельный пул потоков.
💡 Установи тайм-ауты, не превышающие общий SLA.
💡 При ошибке сервиса рекомендаций можно вернуть пустой список.
Решение
Создадим отдельный пул потоков для блокирующих запросов:
@Configuration
public class BffExecutorConfig {
@Bean("bffExecutor")
public Executor bffExecutor() {
ThreadPoolTaskExecutor executor =
new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("bff-");
executor.initialize();
return executor;
}
}
Реализация сервиса:
@Service
public class ClientCartScreenService {
private final ShoppingCartServiceClient cartClient;
private final RecommendationsServiceClient recommendationsClient;
private final Executor executor;
public ClientCartScreenService(
ShoppingCartServiceClient cartClient,
RecommendationsServiceClient recommendationsClient,
@Qualifier("bffExecutor") Executor executor
) {
this.cartClient = cartClient;
this.recommendationsClient = recommendationsClient;
this.executor = executor;
}
public ClientCartScreenDto getScreenData() {
CompletableFuture<CartDto> cartFuture =
CompletableFuture.supplyAsync(
cartClient::getClientCart,
executor
)
.orTimeout(
900,
TimeUnit.MILLISECONDS
);
CompletableFuture<List<RecommendationDto>>
recommendationsFuture =
CompletableFuture.supplyAsync(
recommendationsClient
::getClientRecommendations,
executor
)
.completeOnTimeout(
List.of(),
700,
TimeUnit.MILLISECONDS
)
.exceptionally(error -> List.of());
CartDto cart = cartFuture.join();
List<RecommendationDto> recommendations =
recommendationsFuture.join();
return new ClientCartScreenDto(
cart,
recommendations
);
}
}
Оба запроса запускаются до вызова join(), поэтому выполняются параллельно. Общее время ответа будет примерно равно времени самого медленного запроса, а не сумме времени двух запросов.
Корзина является обязательной частью экрана, поэтому ошибка её получения прерывает выполнение. Рекомендации являются дополнительными данными: при ошибке или превышении тайм-аута возвращается пустой список.
Для Feign-клиентов также необходимо настроить сетевые connect timeout и read timeout. Тайм-аут CompletableFuture ограничивает ожидание результата, но не гарантирует остановку самого HTTP-запроса.