Как связана транзакция с длительной работой внешнего сервиса
Что проверяют
Понимаете ли вы, что транзакция базы и HTTP-вызов живут по разным правилам: вызов не откатывается, а открытая транзакция держит ресурсы. Следом обычно спрашивают, что будет, если внешний сервис ответил успехом, а коммит упал.
Как отвечать
Начните с проблемы. Метод с @Transactional, внутри которого идёт вызов внешнего API на несколько секунд, всё это время:
- держит соединение из пула: у HikariCP по умолчанию
maximumPoolSizeравен 10, и десяток одновременных медленных вызовов останавливает весь сервис; - держит блокировки: строки, обновлённые до вызова, закрыты для других транзакций до коммита;
- мешает базе: в PostgreSQL длинная транзакция задерживает очистку старых версий строк, и таблицы разбухают;
- не даёт атомарности: откат транзакции не отменит списание, которое внешний сервис уже провёл.
Правильный каркас: вынести вызов за пределы транзакции и разбить работу на короткие шаги. Первая транзакция записывает намерение со статусом PENDING и коммитится. Затем вызов внешнего сервиса с таймаутом и ключом идемпотентности. Вторая транзакция фиксирует результат. В Spring это удобно делать через TransactionTemplate или @TransactionalEventListener(phase = AFTER_COMMIT).
Если событие нужно отправить надёжно, назовите transactional outbox: запись в бизнес-таблицу и в таблицу исходящих событий идёт в одной транзакции, а отдельный процесс их публикует. Для цепочки из нескольких сервисов с откатом подойдёт сага с компенсирующими действиями. Распределённая транзакция через XA здесь не поможет: HTTP-сервис не участвует в двухфазном коммите.
Где спотыкаются
Считают, что @Transactional делает внешний вызов частью транзакции и при исключении «всё откатится». Откатится только база.
Повторяют запрос при таймауте без ключа идемпотентности. Сервис мог выполнить операцию, а потерялся только ответ, и повтор списывает деньги второй раз.
Выносят вызов в соседний метод того же бина с @Transactional(propagation = REQUIRES_NEW) и ждут, что граница транзакции поменяется. Вызов через this проходит мимо прокси Spring, и аннотация не срабатывает.