Транзакции в Spring Framework

Транзакции в Spring Framework

https://t.me/sqlhub
Цель написания данной статьи, как и других, была связана с тем, чтобы попытаться более подробно и детально рассказать, как же все-таки работают транзакции в Spring.
В свое время я столкнулся с трудностями, когда мне пришлось более детально применять на практике свои знания, связанные с транзакциями.
В интернете очень много информации на этот счет, но я постарался изложить максимально компактно и в то же время подробно, что же на самом деле происходит под капотом и как это все устроено.
Надеюсь данная статья будет полезна многим.

Для начала небольшое отступление.

Spring поддерживает два типа управления транзакциями

— Программное управление транзакциями — когда разработчик должен сам управлять транзакциями.

— Декларативное управление транзакциями — означает отделение управления транзакциями от бизнес-логики. Разработчик использует только аннотации в конфигурации на основе XML для управления транзакциями.

Spring создает proxy для классов, объявленных аннотацией @Transactional. Proxy в большинстве случаев невидим во время выполнения. Он предоставляет способ для Spring вводить поведение “до”, “после” или “во время” вызовов методов в proxy-объект.

Поэтому, когда мы определяем метод с @Transactional, Spring динамически создает proxy. Когда клиенты совершают вызовы в наш объект, вызовы перехватываются, а поведение будет вводиться через механизм proxy.

Когда Spring загружает определения бина и настроен на поиск аннотаций @Transactional, он создаст эти proxy-объекты вокруг нашего бина. Эти proxy-объекты являются экземплярами классов, которые будут автоматически генерироваться во время выполнения. Поведение этих proxy-объектов по умолчанию при вызове метода — это просто вызвать тот же метод в “target” бин (т.е. наш бин).

Proxy также могут быть снабжены “перехватчиками”, и, когда они присутствуют, эти “перехватчики” будут вызываться proxy-сервером до того, как он вызовет наш целевой метод бина. Для целевого бина, аннотированного с помощью @Transactional, Spring создаст TransactionInterceptor и передаст его созданному proxy-объекту. Поэтому, когда мы вызываем метод из клиентского кода, мы вызываем метод на proxy-объекте, который сначала вызывает TransactionInterceptor (который начинает транзакцию), который, в свою очередь, вызывает метод в нашем целевом бине. Когда вызов завершается, TransactionInterceptor принимает и завершает/откатывает транзакцию.

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

Начиная с версии 3.1 Spring ввел аннотацию @EnableTransactionManagement, которая используется с аннотацией @Configuration над классами и включает поддержку транзакций.

Понятие транзакционная модель

Транзакционная модель функционирует на уровне транзакций. В рамках одного соединения может быть много транзакций. От одного пользователя может быть много одновременных и независимых соединений а также конкурентных транзакций тоже может быть много.

Если взять за основу такие известные реляционные базы как MySQL и PostgreSQL — одно соединение одновременно держать две разные транзакции открытыми не может, т.к. одно соединение — только одна открытая транзакция. Нет возможности в рамках одного соединения выполнять одновременно несколько команд. Они предоставляют неблокирующий вызов, но не дождавшись конца ответа новые запросы отправлять у вас не получится.

Например в MySQL каждый “thread” держит соединение. Он не отслеживает, каким процессом это соединение открыто, т.е. ему без разницы, один процесс открыл множество соединений или это множество процессов. MySQL выполняет запросы параллельно, в рамках “тредной модели”, но существуют блокировки.

Какие бывают транзакционные модели ?

1. Local — голый JDBC

2. Programmatic — JPA

3. Declarative — Spring (когда мы ставим аннотацию)

Интересно, что же происходит внутри Spring при объявлении аннотации @Transactional ?

В Spring есть такое понятие как SpringDefaultNameConvension — именование бинов по умолчанию.

По умолчанию Spring ищет есть ли у нас бин с именем TransactionManager. Если есть, то Spring его найдет и сам подключит, нам не надо его дополнительно настраивать.

Но если у нас несколько TransactionManager, тогда мы должны указать явно. Дальше указываем дополнительные параметры, такие как уровни изоляции транзакций, свойство propagation и т.д.

1. Поставили аннотацию и объявили TransactionManager

TransactionManager создает EntityManager, если он необходим, и осуществляет старт новой транзакции. В зависимости от того, выполняется ли хоть одна транзакция в текущий момент или нет и параметра “propagation” у метода, аннотированного @Transactional, создается новая транзакция.

Алгоритм создания новой транзакции:

— создается новый EntityManager

— EntityManager привязывается к “текущему потоку Thread”

— берется соединение из пула соединений БД

— это соединение привязывается к “текущему потоку Thread” при помощи ThreadLocal (Класс ThreadLocal предоставляет локальные переменные потока. Каждый поток имеет свою собственную инициализированную копию переменной)

@Configuration
@EnableTransactionManagement
public class Config {
@Autowired
EntityManagerFactory factory;
@Autowired
private DataSource dataSource;

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

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

2. Прописываем EntityManagerFactory если используем JPA, прописываем DataSource если JDBC и т.д.

<bean id=”transactionManager” class=”org.springframework.orm.jpa.JpaTransactionManager”>
<property name=”entityManagerFactory” ref=”entityManagerFactory”>
</bean>

или

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

3. Поставить тег <tx:annotation-driven proxy-target-class=”true”/> — именно он запускает весь механизм работы Spring JTA !

По умолчанию: proxy-target-class=”false”.


Что он делает в Spring config ?

Мы знаем что Spring работает по принципу проксирования:

1. Для бинов он создает proxy

2. Используется подход AOP для добавления какого-то поведения.

3.1 Spring начинает работать, выполнять свои классы и первый из них это AopAutoProxyConfigurer, который вызывает метод configureAutoProxyCreator().

3.2 Потом создается класс TransactionInterceptor.

3.3 Далее Spring находит наш TransactionManager и подключает его к TransactionInterceptor.

3.4 Spring регистрирует TransactionInterceptor как бин.

Аннотация @Transactional определяет область действия одной транзакции БД. Транзакция БД происходит внутри области действий persistence context. Persistence context в JPA является EntityManager, который использует внутри класс Session ORM-фреймворка Hibernate (если использовать Hibernate как persistence провайдер).

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

Есть такое понятие как EntityManager proxy.

Например, когда происходит вызов метода entityManager.persist(), он не вызывается напрямую у EntityManager. Вместо этого вызывается прокси, который достает текущий EntityManager из потока, в который его положил менеджер транзакций.

Подход через аннотации намного удобен и удобочитаем и поэтому рекомендуется использовать именно его.

Как @Transactional парсится и что Spring с ней делает ?

У Spring есть специальный парсер для этого — SpringTransactionAnnotationParser (используется по умолчанию).

Что делает TransactionInterceptor ?

1. Считывает атрибуты (параметры), которые были у аннотации @Transactional

txAttr = getTRansactionAttributeSource().
getTransactionAttribute(invocation.getMethod(),
targetClass);

2. Предоставляет интерфейс PlatformTransactionManager — основной интерфейс для всех TransactionManager (TransactionManager взаимодействует непосредственно с БД для управлением транзакций)

PlatformTransactionManager ptm = determineTransactionManager(txAttr);


Spring сам по себе транзакциями не управляет. Он является прослойкой между вашим декларативным описанием и конечной базой данных. Он все делегирует базе данных. Spring — это просто удобный для нас способ объявить или “менеджить” транзакции, но внутри себя Spring не содержит чего-то такого, что можно было бы назвать транзакцией.

3. Идет создание транзакций

TransactionInfo trInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);


В зависимости от того, как мы сконфигурировали атрибуты, транзакцию можно начать или нет.

4. Происходит вызов основного метода

retVal = invocation.proceed();
} catch(Throwable ex) {
completeTransactionAfterThrowinf(trInfo, ex);
}

где invocation.proceed()- вызов основного транзакционного метода с кодом

5. Происходит commit транзакции если нет exception

commitTransactionAfterReturning(trInfo);


Что касается настроек, так в XML мы можем указать такие параметры, как:

все методы, начинающиеся на find, сделать readOnly, а методы, начинающиеся на create получают откат транзакции в случае возникновения например SQLException и не откатываются в случае получения RuntimeException, уровни изоляции и другие.

<tx:advice id=”transactionRule01" transactionManager=”myManager”>
<tx:attributes>
<tx:method name=”find*” read-only=”true”/>
<tx:method name=”create*” rollback-for=”SQLException” no-rollback-for=”RuntimeException” isolation=”SERIALIZABLE” propagation=”MANDATORY”/>
</tx:attributes>
</tx:advice>
Прописываем DataSource, url, login, password и класс драйвера для вашей БД.
<bean id=”myManager” class=”org.springframework.jdbc.datasource.DataSourceTransactionManager”>
<property name=”dataSource” ref=”dataSource”/>
</bean>

Также можно указать для какого конкретно класса можем использовать эти правила, что позволяет более гибко настраивать логику. Указываем, что данное правило будет применяться для всех методов в классе FindInformationService будет использоваться конфигурация transactionRule01.

<aop:pointcut id=”findInformationServiceByUser” expression=”execution(* com.helpdesk.FindInformationService.*(…))”/>
<aop:advisor advice-ref=”transactionRule01" pointcut-ref=”findInformationServiceByUser”>

В Spring есть абстрактный класс AbstractPlatformTransactionManager, который является основным для всех TransactionManager. Он определяет транзакции, применяет уже сконфигурированный уровень propagation, может приостанавливать и возобновлять транзакции, установка флага rollbackOnly для определенного действия (см. выше) и т.д.

Рассмотрим в качестве примера JPATransactionManager.

1. получили EntityManagerFactory

2. получили DataSource

3. исходя из диалекта (какая у нас БД) создается новая транзакция beginTransaction()

т.е. Spring ничего не делает, он просто “говорит” что знает какая у нас БД и предоставляет нам соответствующий для неё диалект и т.д.

4. зарегистрировали полученное соединение в ThreadLocal объекте.

Все соединения и транзакции являются потокозависимыми (все выполняется в текущем потоке), т.е. когда мы начали транзакцию в своем потоке мы не можем ее как-то напрямую расширить на другой поток (в этом случае будет начинаться новая транзакция). Разве что можно отключить транзакцию от потока и “приаттачить” к другому.

5. beginTransaction()

prepareTransaction()

commitTransaction()

Немаловажными компонентами являются интерфейсы TransactionDefinition и TransactionStatus.

TransactionDefinition содержит в себе конфигурацию транзакции, уровень изоляции и propagation.

TransactionStatus возвращает статус текущей транзакции.

Это все конечно хорошо, но как же транзакции работают в приложениях ?

Существует стандарт XA (eXtended Architecture), который связывает наш источник данных (dataSource) с TransactionManager. Базы данных должны реализовывать этот стандарт внутри себя, чтобы TransactionManager мог управлять ими и создавать транзакции.

Этот стандарт поддерживает 2 типа коммитов:

1. однофазный — работает с одним источником данных (1 база данных) — только 1 транзакция — либо выполнилась либо нет.

2. двухфазный — работает с 2 или более источником данных — имеется несколько DataSource, внутри каждого есть своя транзакция и есть 1 глобальная транзакция для этих внутренних транзакций

Транзакции работают в своем потоке.

Есть такой термин как transaction context — он обязательно имеет id, который обозначает, что транзакция работает в таком-то контексте. Также может иметь другие параметры.

Какие бывают TransactionManager ?

1. DataSourceTransactionManager — для JDBC

2. HibernateTransactionManager — для Hibernate

3. JPATransactionManager — для JPA (обычно подходит для приложений, которые используют один JPAEntityManagerFactory для доступа к транзакционным данным)

4. JTATransactionManager — для JTA (обычно необходим для доступа к нескольким транзакционным ресурсам в рамках одной транзакции)

5. JDOTransactionManager

6. JmsTransactionManager

7. WebLogicJtaTransactionManager

8. WebSphereUowTransactionManager — реализация PlatformTransactionManager для WebSphere

9. CallbackPreferringPlatformTransactionManager — предоставляет методы для выполнения обратного вызова в транзакции

10. PlatformTransactionManager — центральный интерфейс в транзакционной инфраструктуре Spring

11. AbstractPlatformTransactionManager — реализует определенное поведение propagation и заботится об обработке синхронизации транзакций

12. ResourceTransactionManager — в основном используется для абстрактного самоанализа менеджера транзакций, давая клиентам подсказку о том, какой менеджер транзакций им был предоставлен и над каким конкретным ресурсом работает менеджер транзакций

13. CciLocalTransactionManager

14. OC4JJtaTransactionManager — вариант JtaTransactionManager для Oracle OC4J (10.1.3 и выше)

По типу TransactionManager делятся на:

1. Локальные транзакции — работают только с одним ресурсом (используют однофазный коммит)

2. Глобальные транзакции — работают с 2 или более ресурсами (используют двухфазный коммит). Используют XA стандарт.

а. JtaTransactionManager

b. OC4JJtaTransactionManager

c. WebLogicJtaTransactionManager

d. WebSphereUowTransactionManager

Они являются двухфазными, т.е. с их помощью можно одновременно управлять транзакциями и в БД и например в JMS. Т.е мы хотим чтобы данные записались в БД и были отправлены в JMS как “одно целое”.

3. Distributed транзакции:

Например мы имеем сложное по структуре приложение, расположенное в разных местах: одна Java машина в одном месте, другая Java машина в другом месте, в третьем третья и т.д.). Это позволяет для всех них иметь одну транзакцию. На данный момент Spring такого сделать не cможет.

С помощью аннотаций можно указать в коде какой менеджер транзакций вы хотите использовать.

Какие есть типы транзакций ?

1) Physical — единая транзакция, которая действует сразу для нескольких методов.

2) Logical — транзакция каждого метода. Логичекие транзакции могут быть внутри одной Physical транзакции. Эти внутренние тарнзакции (Logical) могут влиять на внешнюю (Physical): например, если одна из внутренних транзакций выбросила exception, то вся внешняя транзакция м.б. отменена и т.д.

Про то, что транзакции должны соответствовать принципу ACID и уровни изоляции транзакций я говорить не буду. На просторах интернета немало сказано на этот счет.

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

Оставлю здесь как шпаргалку свойств propagation для транзакций

1. MANDATORY — использует существующую транзакцию. Если ее нет — бросает exception. Если используется для класса, то действует на все public методы.

2. NESTED — вложенная транзакция (подтранзакция). Подтвержается вместе с внешней транзакцией. Если нет существующей транзакции — работает как REQUIRED.

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

3. NEVER — означает, что данный метод не должен выполняться в транзакции. Если транзакция запущена — бросает exception.

4. NOT_SUPPORTED — означает не выполнять в текущей транзакции. Если транзакция запущена — она останавливается на время выполнения метода. Метод выполняется вне транзакции. Когда метод выполнился — транзакция запускается.

5. REQUIRED — (по умолчанию) означает, что если запущена транзакция — выполнять внутри нее, иначе создает новую транзакцию. Если ошибка в запросе, то в базу ничего на запишется.

6. REQUIRES_NEW — создает в любом случае новую транзакцию. Если запущена существующая транзакция — она останавливается на время выполнения метода, новый метод выполняется в новой транзакции, и дальше выполняется внешняя транзакция, если она есть.

7. SUPPORTS — может выполняться внутри транзакции, если она запущена, иначе выполнять без транзакции (новую транзакцию не создает), т.е. методу не важно, будет транзакция или нет, он в любом случае выполнится, но если будет транзакция, то он выполнится внутри нее.

Параметры аннотации @Transactional:

@Transactional(value = “testTransaction”, isolation = Isolation.SERIALIZABLE, propagation = Propagation.SUPPORTS, readOnly = true, timeout = 1, rollbackFor = “IOException”)


где, readOnly — имеет значение только внутри транзакции. Если операция происходит вне контекста транзакции, флаг просто игнорируется

timeout — выставляется именно для транзакций (надо вставлять в том месте, где начинается новая транзакция). По умолчанию, если возникает

RuntimeException — то транзакция будет откатываться. Если checked exception — то транзакция не будет откатываться. По умолчанию используется таймаут, установленный по умолчанию для базовой транзакционной системы.

rollbackFor — указываем роллбэк для определенного exception

rollbackForClassName

noRollbackFor — Указывает, что откат не должен происходить, если целевой метод вызывает исключение, которое вы укажете.

noRollbackForClassName

Интересный момент:

Spring автоматически откатывает транзакции для unchecked (Runtime) исключений:


Например если один транзакционный метод вызывает другой транзакционный метод в другом классе и этот внутренний вызов выбрасывает Runtime exception, то вся транзакция целиком будет отменена. В этом случае можно использовать параметр noRollBackFor или выполнить внутреннюю транзакцию в новой транзакции (свойства propagation).

Аннотация @Transactional будет проигнорирована и не выбросит исключения если применять ее к private, protected или default модификаторами доступа.


С помощью TransactionSynchronizationManager.isActualTransactionActive() можно отслеживать какую-либо внешнюю транзакцию, если она у вас есть.

Надеюсь данная статья будет полезна.



Report Page