Почему @Async с @Transactional на одном методе не работает так, как ожидают

Спрашивают редко напрямую, но часто через практический кейс: «метод асинхронный и транзакционный одновременно — что пойдёт не так». Проверяют понимание того, как Spring оборачивает бины в proxy и в каком порядке применяются перехватчики.

Оба аннотации работают через AOP-proxy. Когда на методе стоят и @Async, и @Transactional, Spring создаёт один proxy, но порядок advice не гарантирован явно — по умолчанию @Async может сработать раньше, и тогда транзакция откроется уже в новом потоке, оторванном от вызывающего контекста. Это ломает распространение транзакции: если метод должен присоединиться к внешней (PROPAGATION.MANDATORY или REQUIRED от вызывающего), он не может — асинхронный поток не видит транзакцию вызывающего потока по определению.

Ещё хуже: если @Async выполняется без явного executor и падает исключение внутри транзакционного блока, откат происходит корректно только для той транзакции, что открылась в новом потоке — а вызывающий код никогда не узнает об ошибке, если не подписан на возвращаемый Future.

@Async @Transactional public void processOrder(Order order) { orderRepository.save(order); notifyWarehouse(order); }

Спросят следом: как правильно разделить асинхронность и транзакционность. Ответ — вынести в два метода в разных бинах: один помечен @Async и просто планирует работу, второй, вызываемый через self-injection или отдельный сервис, помечен @Transactional и выполняется синхронно внутри нового потока. Или явно оборачивать в TransactionTemplate внутри асинхронного кода, где виден момент начала и конца транзакции.

Если на методе одновременно две AOP-аннотации, транзакционная логика должна начинаться уже внутри нового контекста выполнения, а не наследоваться из вызывающего потока.

Тренажёр: 600 вопросов, мок с таймером, план повторов

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


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