Урок по треку Spring и Spring Boot 8 мин

Транзакции и @Transactional

Как работает @Transactional, распространение, изоляция, откат и типичные ловушки.

@Transactional встречается в каждом сервисе, и почти каждый разработчик хоть раз ловил «почему не откатилось». На собесе разбирают механику прокси, распространение, правила отката и изоляцию.

Как это работает

@Transactional обрабатывает прокси (см. модуль про AOP). При вызове метода через прокси TransactionInterceptor:

  1. спрашивает PlatformTransactionManager (для JPA это JpaTransactionManager) о транзакции с нужными атрибутами;
  2. тот берёт соединение из пула, выключает autocommit и привязывает соединение и EntityManager к текущему потоку (TransactionSynchronizationManager, внутри ThreadLocal);
  3. вызывается метод; репозитории находят соединение через поток;
  4. при нормальном выходе commit, при подходящем исключении rollback.

Следствия: транзакция привязана к потоку, поэтому работа в другом потоке (@Async, CompletableFuture, параллельный стрим) идёт вне неё. Вызов через this, private-метод, final-метод не проходят через прокси.

Правила отката

По умолчанию откат при unchecked-исключениях (RuntimeException) и Error. На checked-исключениях (IOException, своё Exception) транзакция коммитится. Настраивается: @Transactional(rollbackFor = Exception.class) или noRollbackFor.

Если исключение поймали внутри метода и не пробросили, прокси о нём не знает и закоммитит.

Распространение (propagation)

ЗначениеПоведение
REQUIRED (по умолчанию)присоединиться к текущей, иначе создать новую
REQUIRES_NEWприостановить текущую и открыть новую, отдельный коммит
NESTEDsavepoint внутри текущей: откат вложенной части не откатывает внешнюю (с JPA через JDBC savepoint, объекты в памяти не откатываются)
SUPPORTSв транзакции, если она есть, иначе без
NOT_SUPPORTEDприостановить текущую и выполнить без транзакции
MANDATORYтребует существующую, иначе исключение
NEVERисключение, если транзакция есть

Ловушка REQUIRED: внутренний метод бросил RuntimeException, внешний его поймал и продолжил. Внутренний прокси уже пометил общую транзакцию как rollback-only, и при коммите внешнего получаем UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only.

REQUIRES_NEW нужен, например, для записи аудита или лога ошибок, которые должны сохраниться даже при откате основной транзакции. Он берёт второе соединение из пула, пока первое ждёт: при маленьком пуле и нагрузке это путь к дедлоку пула. И он, как и всё остальное, не работает при вызове через this.

readOnly

@Transactional(readOnly = true) подсказывает: Hibernate не делает dirty checking и flush (меньше работы и памяти), драйвер может пометить соединение только для чтения, маршрутизация может отправить запрос на реплику. Это не запрет записи на уровне Spring: просто изменения не будут сброшены автоматически.

Изоляция и таймаут

isolation = Isolation.REPEATABLE_READ меняет уровень изоляции для этой транзакции (по умолчанию уровень базы, в PostgreSQL READ COMMITTED). timeout в секундах ограничивает выполнение.

Где ставить @Transactional

  • На методах сервисного слоя, где начинается бизнес-операция. Не в контроллерах и не на каждом методе репозитория.
  • Транзакция должна быть короткой: никаких HTTP-вызовов, отправки писем и ожиданий внутри. Соединение и блокировки строк держатся до конца.
  • Действия «после успешного коммита» (письмо, событие в Kafka) делают через @TransactionalEventListener(phase = AFTER_COMMIT) или паттерн outbox.

Программный вариант: TransactionTemplate.execute(status -> ...), когда нужна транзакция на часть метода или нет прокси.

Частые ошибки

  • Вызов @Transactional-метода из того же класса.
  • Ожидание отката на checked-исключении.
  • Проглоченное исключение внутри метода.
  • REQUIRES_NEW через this и ожидание отдельной транзакции.
  • Долгий внешний вызов внутри транзакции.

Что спросят на собесе

  • Как Spring реализует @Transactional? Где хранится текущая транзакция?
  • При каких исключениях происходит откат по умолчанию?
  • Чем REQUIRED отличается от REQUIRES_NEW и NESTED?
  • Что такое UnexpectedRollbackException и откуда он берётся?
  • Что даёт readOnly = true?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.