К чему может привести вызов внешнего REST клиента внутри транзакции
Что проверяют
Понимаете ли вы, что транзакция базы не распространяется на внешний сервис и что держать соединение во время сетевого вызова опасно. Следом почти всегда спрашивают, как сделать правильно, и ждут outbox или вынос вызова за границу транзакции.
Как отвечать
Разложите последствия на три группы:
- ресурсы: пока идёт HTTP-запрос внутри метода с
@Transactional, поток держит соединение из пула. У HikariCP по умолчаниюmaximumPoolSizeравен 10, и если внешний сервис отвечает по пять секунд, пул кончается за считанные запросы, остальные ждутconnectionTimeout(30 секунд) и падают. Страдают и эндпоинты, которые с этим сервисом никак не связаны; - блокировки: строки, изменённые до вызова, заблокированы до коммита. Растут ожидания и шанс взаимоблокировок, а в PostgreSQL долгая транзакция мешает
VACUUMчистить мёртвые версии строк; - согласованность: откат не отменяет HTTP-вызов. Платёж прошёл, коммит упал, заказа нет. Или клиент словил таймаут, транзакция откатилась, а на той стороне запрос выполнился, и повтор создаст дубль.
Решения. Вызов переносят до или после короткой транзакции. Если он должен идти строго после записи, есть @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT), но при падении приложения между коммитом и вызовом событие теряется. Надёжный вариант это transactional outbox: в той же транзакции пишется строка в таблицу outbox, а отдельный процесс или Debezium доставляет её и повторяет при ошибках. Принимающей стороне нужен ключ идемпотентности. Для цепочки сервисов используют сагу с компенсациями. И всегда явные таймауты плюс circuit breaker, например из Resilience4j.
Где спотыкаются
Считают, что при исключении откатится всё, включая вызов. Откатывается только база.
Оборачивают вызов в Propagation.REQUIRES_NEW. Внешняя транзакция лишь приостанавливается: её соединение и блокировки держатся весь вызов, а новая транзакция обычно берёт из пула второе соединение.
Разносят логику по двум методам одного бина и ставят @Transactional на внутренний. Вызов через this идёт мимо прокси, и аннотация не срабатывает.