Как на самом деле работает @Transactional Spring?

Введение

В этой статье мы опустимся в дебри менеджмента транзакций фреймворка Spring. Мы рассмотрим как устроена под «капотом» работа аннотации @Transactional.

JPA и управление транзакциями

Важно отметить, что спецификация JPA сама по себе не предоставляет никакого декларативного управления транзакциями. При использовании JPA вне DI (Dependency Injection) контейнера, транзакции управляются программно самим разработчиком:


UserTransaction utx = entityManager.getTransaction();

try {
   utx.begin();

   businessLogic();

   utx.commit();
} catch(Exception ex) {
  utx.rollback();
  throw ex;
}

Такой способ контроля за транзакциями придает прозрачности коду, но есть несколько недостатков:

  • Много повторяющегося кода и склонность к ошибочному поведению
  • Любая ошибка имеет очень большое влияние
  • Ошибки трудно отладить и воспроизвести
  • Усложняет удобочитаемость кода
  • Что делать если этот метода вызывает другой метод с транзакциями?

Используем аннотацию Spring @Transactional

C аннотацией @Transactional, пример можно упростить следующим образом:


@Transactional
public void businessLogic() {
... use entity manager inside a transaction ...
}

Такой код намного удобен и удобочитаем, и на данный момент рекомендуется использовать такой подход для управления транзакциями в Spring.

Используя @Transactional, многие важные аспекты как распространяемость транзакций (propagation) обрабатываются автоматически. в этом случае, если другой метод с транзакцией вызовется методом businessLogic(), этот метод присоединится к выполняемой транзакции.

Один недостаток все таки имеется: сложно понять как работает изнутри этот мощный механизм и сложно отлаживать при возникновении ошибки.

Что значит @Transactional?

Одно из ключевых знаний о @Transactional это то, что есть две отдельных концепции для рассмотрения, каждая из которых имеет свою область действий и жизненный цикл:

  • persistence контекст
  • транзакция БД

Аннотация сама по себе определяет область действия одной транзакции БД. Транзакция БД происходит внутри области действий persistence context.

Persistence контекстом в JPA является EntityManager, который использует внутри класс Session ORM-фреймворка Hibernate (при использовании Hibernate как persistence провайдера).

Persistence контекст это объект-синхронайзер, который отслеживает состояния ограниченного набора Java объектов и синхронизирует изменения состояний этих объектов с состоянием соответствующих записей в БД.

Один объект Entity Manager не всегда соответствует одной транзакции БД. Один объект Entity Manager может быть использован несколькими транзакциями БД.

Когда EntityManager охватывает множество транзакций БД?

Самый частый случай происходит когда приложение использует шаблон «Open Session in View» для предотвращения исключений «ленивой» инициализации.

В этом случае запросы, которые могли быть выполнены одной транзакцией при вызове из слоя сервиса, выполняются в отдельных транзакциях в слое View, но они совершаются через один и тот же Entity Manager.

Другой случай это  когда persistence context отмечен разработчиком как PersistenceContextType.EXTENDED, что означает что в приложении используется антипаттерн «одна сессия на приложение».

Что определяет отношение между EntityManager и транзакцией?

Конечно каждый разработчик решает сам, но самый частый выбор это использовать JPA Entity Manager со стратегией «один Entity Manager на одну транзакцию приложения». Следующий код демонстрирует, как можно внедрить Entity Manager в Bean:

@PersistenceContext
private EntityManager em;

В этом примере по умолчанию работает режим «один Entity Manager на одну транзакцию приложения». В этом режиме, если мы используем Entity Manager внутри метода помеченного аннотацией @Transactional, то этот метод будет выполняться в единой транзакции БД.

Как работает @PersistenceContext?

Один вопрос приходит в голову, как может @PersistenceContext внедрить entity manager один раз при старте контейнера, учитывая что время жизни entity менеджеров очень мало, и обычно  их требуется несколько на один запрос?

Ответ такой: не может. Entity Manager это интерфейс, и то что внедряется в бин не является самим по себе entity менеджером, это context aware proxy, который будет делегировать к конкретному entity менеджеру в рантайме.

Обычно этот конкретный класс используемый для прокси это SharedEntityManagerInvocationHandler, в чем можно убедиться с помощью дебаггера.

Как тогда работает @Transactional?

Прокси persistence контекста, которое имлементирует EntityManager не является достаточным набором компонентов для осуществления декларативного управления транзакциями. На самом деле нужно три компонента:

  • Прокси Entity менеджера
  • Аспект транзакций
  • Менеджер транзакций

Давайте рассмотрим каждый и посмотрим их взаимодействие.

Аспект транзакций

Аспект транзакций — «around» аспект, который вызывается и до и после выполнения аннотированного бизнес метода. Конкретный класс для имплементации этого аспекта это TransactionInterceptor. 

Аспект транзакций имеет две главные функции:

  • В момент «до» аспект определяет выполнить ли выполняемый метод в пределах уже сущестувующей транзакции БД или должна стартовать новая отдельная транзакция.
  • В момент «после» аспект решает что делать с транзакцией, делать коммит, откат или оставить незакрытой.

В момент «до» аспект сам не содержит логики по принятию решения, решение начать новую транзакцию, если это нужно, делегируется Transaction менеджеру.

Transaction менеджер

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

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

Ответы необходимы предоставить в момент когда вызывается логика аспекта транзакций в момент «до».  Менеджер транзакций принимает решение, основываясь на следующих фактах:

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

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

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

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

Они хранятся в потоке, пока выполняется транзакция, и затем передаются менеджеру транзакций для очистки, когда они уже будут не нужны.

Любая часть программы, которой нужен текущий entity manager или соединение, может заполучить их из потока. Этим компонентом программы, который делает именно так является Entity Manager Proxy.

EntityManager proxy

Прокси Entity менеджера (который был представлен ранее) это последний кусоче паззла. Когда бизнес метод делает вызов, например, entityManager.persist(), этот вызов не вызывается напрямую у entity менеджера.

Вместо этого бизнес метод вызывает прокси, который достает текущий entity менеджер  из потока, в который его положил менеджер транзакций.

Зная теперь все части механизма @Transactional, давайте пройдемся по обычной конфигурации Spring, необходимой для работы всего этого.

Сложим весь паззл вместе

Давайте опишем конфигурацию трех компонентов, необходимых для правильной работы аннотации @Transactional. Начнем с определения Entity Manager Factory.

Это позволит инжектировать прокси Entity менеджера через аннотацию @PersistenceContext:


@Configuration
public class EntityManagerFactoriesConfiguration {
@Autowired
private DataSource dataSource;

@Bean(name = "entityManagerFactory")
public LocalContainerEntityManagerFactoryBean emf() {
    LocalContainerEntityManagerFactoryBean emf = ...
    emf.setDataSource(dataSource);
    emf.setPackagesToScan(
      new String[] {"your.package"});
    emf.setJpaVendorAdapter(
      new HibernateJpaVendorAdapter());
    return emf;
   }
}

Слудующим шагом сконфигурируем Менеджер Транзакций и применим Аспект транзакций к классам аннотированным @Transactional:


@Configuration
@EnableTransactionManagement
public class TransactionManagersConfig {
@Autowired
EntityManagerFactory emf;
@Autowired
private DataSource dataSource;

@Bean(name = "transactionManager")
public PlatformTransactionManager transactionManager() {
   JpaTransactionManager tm =
         new JpaTransactionManager();
   tm.setEntityManagerFactory(emf);
   tm.setDataSource(dataSource);
   return tm;
   }
}

Аннотация @EnableTransactionManagement указывает Спринг, что классы с аннотацией @Transactional, должны быть обернуты аспектом транзакций. Теперь можно использовать аннотацию @Transactional.

Заключение

Механизм декларативного управления транзакциями, представляемый фреймворком Spring, конечно мощный, но он может быть неправильно использован и можно легко ошибиться при конфигурации.

Понимание как он работает очень полезно при решении возникающих проблем, когда механизм работает не так как ожидается.

Самая главная вещь, которую нужно всегда держать в голове, это то что есть две концепции, которые нужно принимать во внимание: транзакция БД и persistence контекст, каждая со своим собственным неочевидным жизненным циклом.

(Visited 17 351 times, 39 visits today)

One thought to “Как на самом деле работает @Transactional Spring?”

Добавить комментарий

Этот сайт использует Akismet для борьбы со спамом. Узнайте как обрабатываются ваши данные комментариев.