Как избежать проблем с долгими ответами внешнего сервиса при открытой транзакции и соединении с базой
Что проверяют
Понимаете ли вы, что открытая транзакция держит соединение из пула и блокировки, а сетевой вызов внутри неё превращает чужую медлительность в вашу аварию. Следом обычно спрашивают: а если вызов внешнего сервиса и запись в базу должны быть согласованы?
Как отвечать
Начните с механики. В Spring @Transactional берёт соединение из пула (по умолчанию HikariCP, maximumPoolSize равен 10) в начале метода и отдаёт его только после коммита. Если внутри идёт HTTP-вызов на 30 секунд, соединение простаивает, строки остаются заблокированы, а при десятке таких запросов пул кончается и падают даже запросы, которым внешний сервис не нужен.
Каркас решения:
- вынести вызов за границу транзакции: сначала сходить во внешний сервис, потом короткой транзакцией записать результат. Или наоборот: записать намерение, закоммитить, потом звонить;
- таймауты везде: у
java.net.http.HttpClientэтоconnectTimeoutв билдере иtimeoutуHttpRequest, плюс@Transactional(timeout = ...)как страховка; - transactional outbox: запись в бизнес-таблицу и в таблицу
outboxв одной транзакции, отдельный процесс отправляет сообщения и повторяет при сбоях. Согласованность есть, соединение не удерживается; - устойчивость вызова: circuit breaker, retry с экспоненциальной задержкой, bulkhead из Resilience4j, чтобы деградация внешнего сервиса не съела все потоки;
- асинхронность: если ответ не нужен пользователю сразу, ставьте задачу в очередь и возвращайте статус «в обработке».
Для цепочки из нескольких сервисов уместно упомянуть сагу с компенсирующими действиями.
Где спотыкаются
Предлагают CompletableFuture.supplyAsync внутри транзакционного метода и считают проблему решённой. Если метод потом ждёт join(), соединение держится так же, а без своего executor задача уходит в общий ForkJoinPool.
Ставят только connectTimeout. Он ограничивает установку соединения, а сервис, который принял запрос и молчит, повесит поток на всё время чтения.
Надеются на таймаут транзакции Spring. Он не прерывает HTTP-вызов посреди метода, а срабатывает только при следующем обращении к базе, так что внешнюю задержку он не ограничивает.