@Transactional и исключение в отдельном потоке

Метод помечен @Transactional, внутри запускает CompletableFuture.supplyAsync, который бросает исключение. Кандидат уверенно говорит: Spring увидит исключение и откатит транзакцию. Это неверно.

supplyAsync выполняется в ForkJoinPool.commonPool() — в другом потоке, без контекста транзакции. AOP-прокси Spring перехватывает исключение только в том потоке, где вызван сам транзакционный метод. Если основной метод уже вернул управление и успешно завершился, прокси видит нормальный выход и коммитит.

Исключение из асинхронной задачи улетает в CompletableFuture и останется там, если никто не вызовет get() или join(), либо не прицепит exceptionally/handle.

@Transactional public void process(Order order) { repository.save(order); CompletableFuture.supplyAsync(() -> externalCall(order)); // исключение здесь транзакцию не откатит }

Спросят следом: как сделать так, чтобы сбой во внешнем вызове реально откатывал сохранение заказа. Правильный путь — либо убрать асинхронность из транзакционной границы и вызывать внешний сервис синхронно внутри той же транзакции, либо явно дожидаться future через join() до коммита, либо выносить внешний вызов за границу транзакции совсем и обрабатывать несогласованность отдельным механизмом (outbox, компенсация).

Транзакция Spring живёт в потоке, а не в бизнес-операции — асинхронный код выходит из-под её защиты незаметно.

senior·base — что спрашивают на самом деле


В этом посте были ссылки, но мы их удалили по правилам Сетки