23. Как спринг работает с транзакциями? Расскажите про аннотацию @Transactional

23. Как спринг работает с транзакциями? Расскажите про аннотацию @Transactional

UNKNOWN

Для работы с транзакциями Spring Framework использует AOP-прокси.

Для  включения  возможности  управления  транзакциями  первым  делом  нужно разместить аннотацию @EnableTransactionManagement у класса-конфигурации @Configuration.

Аннотация  @EnableTransactionManagement  означает,  что  классы,  помеченные @Transactional,  должны  быть  обернуты  аспектом  транзакций.  Однако,  если  мы  используем Spring Boot и имеем зависимости spring-data-* или spring-tx, то управление транзакциями будет включено по умолчанию. 

@EnableTransactionManagement  отвечает  за  регистрацию  необходимых  компонентов Spring,  таких  как  TransactionInterceptor  и  советы  прокси  (proxy  advices-  набор  инструкций, выполняемых на точках среза - Pointcut). Регистрируемые компоненты помещают перехватчик в стек вызовов при вызове методов @Transactional.

Spring создает прокси для всех классов, помеченных @Transactional (либо если любой из методов  класса  помечен  этой  аннотацией).  Прокси-объекты  позволяют  Spring  Framework вводить транзакционную логику до и после вызываемого метода - главным образом для запуска и коммита/отката транзакции.

Если мы разместим аннотацию @Transactional над классом @Service, то все его методы станут  транзакционными.  Так,  при  вызове,  например,  метода  save()  произойдет  примерно следующее:

  1. Вначале мы имеем:
  • класс  TransactionInterceptor,  у  которого  основной  метод  invoke(...),  внутри которого  вызывается  метод  класса-родителя  TransactionAspectSupport: invokeWithinTransaction(...), в рамках которого происходит магия транзакций.
  • TransactionManager:  решает,  создавать  ли  новый  EntityManager  и/или транзакцию.
  • EntityManager proxy: EntityManager - это интерфейс, и то, что внедряется в бин в слое  DAO  на  самом  деле  не  является  реализацией  EntityManager.  В  это  поле внедряется  EntityManager  proxy,  который  будет  перехватывать  обращение  к полю EntityManager и делегировать выполнение конкретному EntityManager в рантайме.  Обычно  EntityManager  proxy  представлен  классом SharedEntityManagerInvocationHandler.
  1. Transaction Interceptor

В  TransactionInterceptor  отработает  код  до  работы  метода  save(),  в  котором  будет определено, выполнить ли метод save() в пределах уже существующей транзакции БД или  должна  стартовать  новая  отдельная  транзакция.  TransactionInterceptor  сам  не содержит логики по принятию решения, решение начать новую транзакцию, если это нужно,  делегируется  TransactionManager.  Грубо  говоря,  на  данном  этапе  наш  метод будет обёрнут в try-catch и будет добавлена логика до его вызова и после:

     try {

        transaction.begin();

        // логикадо

        service.save();

        // логикапосле

        transaction.commit();

     } catch(Exception ex) {

       transaction.rollback();

       throw ex;

     }

  1. TransactionManager

Менеджер транзакций должен предоставить ответ на два вопроса:

  • Должен ли создаться новый EntityManager?
  • Должна ли стартовать новая транзакция БД?

TransactionManager принимает решение, основываясь на следующих фактах:

  • выполняется ли хоть одна транзакция в текущий момент или нет;
  • атрибута «propagation» у метода, аннотированного @Transactional (для примера, значение REQUIRES_NEW всегда стартует новую транзакцию).

Если TransactionManager решил создать новую транзакцию, тогда:

  • Создается новый EntityManager;
  • EntityManager «привязывается» к текущему потоку (Thread);
  • «Получается» соединение из пула соединений БД;
  • Соединение «привязывается» к текущему потоку.

И  EntityManager  и  это  соединение    привязываются  к  текущему  потоку,  используя переменные ThreadLocal.

  1. EntityManager proxy

Когда метод save() слоя Service делает вызов метода save() слоя DAO, внутри которого вызывается, например, entityManager.persist(), то не происходит вызов метода persist() напрямую у EntityManager, записанного в поле класса DAO. Вместо этого метод вызывает EntityManager proxy, который достает текущий EntityManager для нашего потока, и у него вызывается метод persist().

  1. Отрабатывает DAO-метод save().
  2. TransactionInterceptor

Отработает  код  после  работы  метода  save(),  а  именно  будет  принято  решение  по коммиту/откату транзакции.

Кроме того, если мы в рамках одного метода сервиса обращаемся не только к методу save(), а к разным методам Service и DAO, то все они буду работать в рамках одной транзакции, которая оборачивает этот метод сервиса.

Вся работа происходит через прокси-объекты разных классов. Представим, что у нас в классе  сервиса  только  один  метод  с  аннотацией  @Transactional,  а  остальные  нет. 

Если  мы вызовем метод с @Transactional, из которого вызовем метод без @Transactional, то оба будут отработаны в рамках прокси и будут обернуты в нашу транзакционную логику.

Однако, если мы вызовем метод без @Transactional, из которого вызовем метод с @Transactional, то они уже не будут работать в рамках прокси и не будут обернуты в нашу транзакционную логику.

У @Transactional есть ряд параметров:

  • @Transactional (isolation=Isolation.READ_COMMITTED) - уровень изоляции.
  • @Transactional(timeout=60)  -  По  умолчанию  используется  таймаут,  установленный  по умолчанию  для  базовой  транзакционной  системы.  Сообщает  TransactionManager-у  о продолжительности  времени,  чтобы  дождаться  простоя  транзакции,  прежде  чем принять решение об откате не отвечающих транзакций.
  • @Transactional(propagation=Propagation.REQUIRED)  -  (Если  не  указано, распространяющееся поведение по умолчанию — REQUIRED.) Указывает, что целевой метод не может работать без другой транзакции. Если до вызова этого метода уже была запущена транзакция, то метод будет работать в той же транзакции, если транзакции не было, то будет создана новая.

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

MANDATORY - Указывает, что для целевого метода требуется активная транзакция. Если активной транзакции нет, метод не сработает и будет выброшено исключение.

SUPPORTS - Указывает, что целевой метод может выполняться независимо от наличия транзакции. Если транзакция работает, он будет участвовать в той же транзакции. Если транзакции  нет,  он  всё  равно  будет  выполняться,  если  не  будет  ошибок.  Методы, которые извлекают данные, являются лучшими кандидатами для этой опции.

NOT_SUPPORTED - Указывает, что целевой метод не требует распространения контекста транзакции. В основном те методы, которые выполняются в транзакции, но выполняют операции с оперативной памятью, являются лучшими кандидатами для этой опции.

NEVER - Указывает, что целевой метод вызовет исключение, если выполняется  в транзакционном  процессе.  Этот  вариант  в  большинстве  случаев  не  используется  в проектах.

  • @Transactional  (rollbackFor=Exception.class)  -  Значение  по  умолчанию: rollbackFor=RunTimeException.class В Spring все классы API бросают RuntimeException, это означает,  что  если  какой-либо  метод  не  выполняется,  контейнер  всегда  откатывает текущую  транзакцию.  Проблема  заключается  только  в  проверяемых  исключениях. Таким  образом,  этот  параметр  можно  использовать  для  декларативного  отката транзакции, если происходит Checked Exception.
  • @Transactional  (noRollbackFor=IllegalStateException.class)  -  Указывает,  что  откат  не должен  происходить,  если  целевой  метод  вызывает  это  исключение.  Если  внутри метода  с  @Transactional  есть  другой  метод  с  аннотацией  @Transactional  (вложенная транзакция),  то  отработает  только  первая  (в  которую  вложена),  из-за  особенностей создания proxy.

Предыдущий вопрос: 22. Что такое АОП? Как реализовано в спринге?

Следующий вопрос: 24. Расскажите про паттерн MVC, как он реализован в Spring?

Все вопросы по теме: список

Все темы: список

Вопросы/замечания/предложения/нашли ошибку: напишите мне

Report Page