Как работает Propagation.REQUIRES_NEW в Spring транзакциях при вызове метода B из метода A
Что проверяют
Понимаете ли вы, как Spring реализует транзакции через прокси и что происходит с внешней транзакцией и соединениями. Ключевой момент, ради которого вопрос и задают: где находится метод B. Следом спрашивают, что будет, если B бросит исключение.
Как отвечать
@Transactional работает через прокси. Когда A вызывает B у другого бина, вызов проходит через прокси, и TransactionInterceptor видит REQUIRES_NEW:
- приостановка: транзакция A отвязывается от потока, но её соединение остаётся занятым;
- новая транзакция: из пула берётся второе соединение, у B свой
EntityManagerи свой коммит; - завершение B: коммит или откат, затем транзакция A восстанавливается и продолжает работу.
Следствия, которые стоит проговорить:
- независимость: если после B упадёт A, изменения B останутся в базе. Этим пользуются для журнала аудита или записи статуса попытки, которая должна сохраниться при любом исходе;
- исключение из B: B откатывается, а исключение летит в A. Не поймаете его, откатится и A. По умолчанию откат вызывают
RuntimeExceptionиError, проверяемые исключения его не вызывают; - видимость: B работает в отдельной транзакции и не видит незакоммиченных изменений A;
- сравнение с
NESTED: там одна физическая транзакция и savepoint, откат вложенной части не трогает внешнюю, но зафиксировать вложенную без внешней нельзя.
Где спотыкаются
Вызывают B из A в том же классе через this. Прокси в вызове не участвует, аннотация на B игнорируется, и B выполняется в транзакции A. Решение: вынести B в отдельный бин или использовать TransactionTemplate.
Не учитывают блокировки. Если A обновил строку, а B пытается изменить ту же строку, B ждёт блокировку, которую A держит до своего конца, а A ждёт B. Поток зависает до таймаута блокировки.
Забывают про пул соединений. Каждый поток внутри REQUIRES_NEW держит два соединения, и под нагрузкой пул HikariCP (по умолчанию 10) исчерпывается: все потоки держат внешние соединения и ждут внутренних.