Spring Framework обеспечивает обширную поддержку транзакций. Но прежде чем мы углубимся в концепции управления транзакциями, давайте разберемся с основной концепцией транзакции.
Транзакция базы данных (СУБД) — это серия из одной или нескольких операций, выполняемых как единая атомарная единица работы. Это означает, что либо все операции в транзакции завершаются успешно, либо ни одна из них не применяется к базе данных. Транзакция может состоять из одной команды, группы команд или любых других действий с базой данных. Любая СУБД, поддерживающая транзакции, должна гарантировать качество ACID для сохранения целостности данных.
ACID (от англ. atomicity, consistency, isolation, durability) — набор требований к транзакционной системе, обеспечивающий наиболее надёжную и предсказуемую её работу.
Основные требования:
-
Атомарность(atomicity). Гарантирует, что каждая транзакция будет выполнена полностью или не будет выполнена совсем. Не допускаются промежуточные состояния.
-
Согласованность(consistency). Требование, подразумевающее, что в результате работы транзакции данные будут допустимыми. Например, если количество денег на счёте не может быть отрицательным, логика транзакции должна проверять, не выйдет ли в результате отрицательных значений.
-
Изолированность(isolation). Гарантия того, что параллельные транзакции не будут оказывать влияния на результат других транзакций.
-
Долговечность(durability). Изменения, получившиеся в результате транзакции, должны оставаться сохранёнными вне зависимости от каких-либо сбоев. Иначе говоря, если пользователь получил сигнал о завершении транзакции, он может быть уверен, что данные сохранены.
Прежде чем мы поймем, что Spring предлагает для управления транзакциями "из коробки", мы должны понять, как работает обычная транзакция JDBC. Простой стандартный код управления транзакциями JDBC выглядит примерно так, как показано ниже:
Connection connection = dataSource.getConnection();
try (connection) {
connection.setAutoCommit(false);
// execute some SQL queries...
connection.commit();
} catch (SQLException e) {
connection.rollback();
}
В этом коде строчка 1, Connection connection = dataSource.getConnection();
обеспечивает подключение к базе данных.
Строка 4, connection.setAutoCommit(false);
запускает обычную транзакцию. Это единственный способ запустить транзакцию базы данных в Java. setAutoCommit(true)
гарантирует, что каждый отдельный оператор SQL автоматически будет включен в транзакцию, а setAutoCommit(false)
- соответственно полная противоположность. Следует отметить, что флаг автоматической фиксации действителен в течение всего времени, пока соединение открыто. Таким образом, нам просто нужно вызвать этот метод один раз, а не повторно.
Строка 6, connection.commit();
сохраняет транзакцию в базе данных, а строка 9 connection.rollback();
, в случае возникновения ошибки в процесее транзакции, отменяет все сделанные в рамках транзакции изменения.
Различные типы управления транзакциями
Spring поддерживает два типа управления транзакциями:
-
Программное управление транзакциями - это означает, что вы должны использовать программирование для управления транзакциями, как мы делали в примере выше. Хотя это обеспечивает большую гибкость, за этим сложно уследить.
-
Декларативное управление транзакциями - это означает, что мы разделяем бизнес-код и управление транзакциями. Для управления транзакциями используются аннотации или настройки на основе XML.
Давайте рассмотрим каждый из этих типов управления транзакциями в Spring.
Программное управление транзакциями
Платформа Spring предоставляет два способа программного управления транзакциями:
-
Использование
TransactionTemplate
. -
Прямая реализация
TransactionManager
.
TransactionTemplate
в Spring Framework — класс для упрощения программного разграничения транзакций и обработки исключений. Позволяет писать объекты низкоуровневого доступа к данным.
TransactionTemplate
и другие шаблоны Spring, такие как JdbcTemplate
, работают по одинаковой методологии. В них используется метод обратного вызова (callback) и создается код, основанный на намерениях, что означает, что он фокусируется только на том, что вы хотите сделать.
@Service
public class EntityService {
@Autowired
private TransactionTemplate template;
public Long registerEntity(Entity entity) {
Long id = template.execute(status -> {
// execute some SQL statements like
// inserting an entity into the db
// and return the autogenerated id
return id;
});
}
}
Если мы сравним это с простой транзакцией JDBC, которую мы обсуждали ранее, нам не придется самим открывать и закрывать соединения с базой данных. Spring также преобразует исключения SQL в исключения времени выполнения. Что касается интеграции с Spring, то TransactionTemplate
будет использовать внутренний менеджер транзакций, который снова будет использовать источник данных. Поскольку все это компоненты в нашей конфигурации контекста Spring, нам не нужно беспокоиться об этом.
Если мы используем TransactionManager
, Spring предоставляет платформу TransactionManager
для императивных и ReactiveTransactionManager
для реактивных транзакций. Мы можем просто инициировать, фиксировать или откатывать транзакции, используя эти менеджеры транзакций.
Декларативное управление транзакциями
В отличие от программного подхода, декларативное управление транзакциями в Spring позволяет управлять транзакциями на основе конфигурации. Декларативные транзакции позволяют разделить транзакции и бизнес-код. Таким образом, для управления транзакциями, мы можем использовать настройки XML или подход, основанный на аннотациях.
Когда XML-конфигурация для приложений Spring была стандартом, транзакции настраивались напрямую с помощью XML. В настоящее время аннотация @Transactional,
благодаря своей простоте, в основном заменила этот похдод. Тем не менее отдельные, устаревшие бизнес-приложения продолжают использовать XML-конфигурацию.
В этом руководстве мы не будем подробно останавливаться на настройке XML, однако частично затронем и его. Здесь мы будем использовать подход AOP:
<tx:advice id="txAdvice" transaction-manager="txManager">
<!-- the transactional semantics... -->
<tx:attributes>
<!-- all methods starting with 'get' are read-only -->
<tx:method name="get*" read-only="true"/>
<!-- other methods use the default transaction settings -->
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
Во-первых, нам нужно использовать тег <tx: advice />
для создания рекомендации по обработке транзакций.
<aop:config>
<aop:pointcut id="entityServiceOperation" expression="execution(* x.y.service.EntityService.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="entityServiceOperation"/>
</aop:config>
<bean id="entityService" class="x.y.service.EntityService"/>
Во-вторых, необходимо определить pointcut, который соответствует методам, которые мы хотим включить в транзакцию, и передать его в bean.
public class EntityService {
public Long registerEntity(Entity entity) {
// execute some SQL statements like
// inserting an entity into the db
// and return the autogenerated id
return id;
}
}
В-третьих, мы можем определить метод на уровне сервиса для добавления бизнес-логики.
У использования XML-конфигураций, есть множество минусов, это избыточность, усложнение читаемости и поддержки кода, ограниченная поддержка XML, инструментами разработки, такими как IDE и т. д. Таким образом аннотации уверенно вытесняют XML - конфигурации.
Аннотация Spring's @Transactional
Теперь давайте посмотрим, как выглядит современное управление транзакциями Spring. Spring по своей сути является контейнером IoC. Таким образом, у него есть преимущество. Spring создает для нас экземпляр Entity Service и обеспечивает автоматическое подключение его к любому другому компоненту, который в нем нуждается.
Теперь всякий раз, когда мы используем аннотацию @Transactional
для компонента, Spring использует небольшую хитрость. Spring не только создает экземпляр EntityService, но и создает транзакционный прокси-сервер того же компонента:
Как мы можем видеть из приведенной выше диаграммы, прокси-сервер выполняет две задачи:
-
Открытие и закрытие подключений/транзакций к базе данных.
-
А затем делегирование исходной службе сущностей.
Другие компоненты, такие как наш EntityController на диаграмме выше, никогда не узнают, что они обращаются к прокси-серверу, а не к настоящему компоненту.
Если мы разберемся более подробно, то обнаружим, что наш EntityService проксируется на лету, но не прокси-сервер обрабатывает состояния транзакций (открытие, фиксация, закрытие, откат). Вместо этого прокси-сервер делегирует задание менеджеру транзакций.
Spring предлагает интерфейс PlatformTransactionManager
/TransactionManager
для управления транзакциями, который по умолчанию поставляется с несколькими удобными реализациями. Одна из них - диспетчер транзакций datasource. Теперь у всех менеджеров транзакций есть такие методы, как doBegin()
или doCommit()
, которые отвечают за подключение и окончательное выполнение.
Резюмируя:
-
Если Spring обнаруживает аннотацию
@Transactional
в компоненте, он создает динамический прокси-сервер для этого компонента. -
Затем прокси-сервер получит доступ к диспетчеру транзакций, который будет открывать и закрывать транзакции/соединения.
-
Наконец, диспетчер транзакций просто сделает то, что мы делали в рамках нашей простой старой реализации подключения JDBC.
Настройка менеджера транзакций
Spring рекомендует определить аннотацию @EnableTransactionManagement
в классе @Configuration
, чтобы включить поддержку транзакций.
@Configuration
@EnableTransactionManagement
public class JPAConfig{
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
//...
}
@Bean
public PlatformTransactionManager transactionManager() {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(entityManagerFactory().getObject());
return transactionManager;
}
}
Однако, если мы используем проект Spring Boot и определили зависимости “spring-data-*” или “spring-tx” от пути к классу, то управление транзакциями будет включено по умолчанию.
Использование аннотации @Transactional
Мы можем использовать аннотации к определениям интерфейсов, классов или непосредственно к методам. Они имеют приоритет друг над другом в соответствии с порядком приоритета от низшего к высшему, например, интерфейс, суперкласс, класс, метод интерфейса, метод суперкласса и, наконец, метод класса.
Следует отметить, что если мы применим эту аннотацию к классу, то она будет применена ко всем открытым методам в нем, которые не были аннотированы аннотацией @Transactional
.
Однако, если мы поместим аннотацию в частный или защищенный метод, Spring проигнорирует ее без ошибки.
Давайте рассмотрим интерфейс, определенный как транзакционный с помощью аннотации:
@Transactional
public interface PaymentService {
void pay(String source, String destination, double val);
}
Далее, мы можем поместить ту же аннотацию в класс, чтобы переопределить параметр транзакции интерфейса:
@Service
@Transactional
public class PaymentServiceImpl implements PaymentService {
@Override
public void pay(String source, String destination, double val) {
// ...
}
}
Наконец, мы можем переопределить все это, установив аннотацию непосредственно в методе:
@Transactional
public void pay(String source, String destination, double val) {
// ...
}
Уровни распространения в транзакциях Spring
Как следует из названия, распространение в транзакции Spring указывает, хочет ли какая-либо служба участвовать в транзакции или нет. Это также определяет поведение компонента или службы в зависимости от того, была ли транзакция уже сгенерирована вызывающим компонентом или службой.
Сначала мы рассмотрим два сценария. В первом сценарии у нас будет определенная сущность register()
, которую мы видели выше, с аннотацией к распространению транзакции:
@Service
public class EntityService {
@Transactional(propagation = Propagation.REQUIRED)
public Long registerEntity(Entity entity) {
// execute some SQL statements like
// inserting an entity into the db
// and return the autogenerated id
return id;
}
}
В другом сценарии предположим, что этот метод registerEntity()
вызывается другой службой OrganizationService
, тогда этот класс будет аннотирован следующим образом:
@Service
@Transactional(propagation=Propagation.REQUIRED)
public class OrganizationService {
@Autowired
EntityService entityService;
public void organize() {
// ...
entityService.registerEntity(entity);
// ...
}
}
Давайте разберемся с каждой из этих стратегий распространения, используя приведенные выше сценарии:
-
Propagation.REQUIRED. Применяется по умолчанию. При входе в метод с аннотацией @Transactional будет использована уже существующая транзакция или создана новая, если никакой ещё нет. Если метод
registerEntity()
вызывается напрямую, он создает новую транзакцию. Принимая во внимание, что если этот метод вызывается изOrganizationService
, поскольку эта служба помечена как@Transactional
, то транзакция будет использовать существующую транзакцию, вызванную на уровне сервиса, а не ту, которая определена вregisterEntity()
. Если у вызывающей службы не была определена транзакция, она создаст новую транзакцию. -
Propagation.SUPPORTS. Метод с этим правилом будет использовать текущую транзакцию, если она есть, либо будет исполняться без транзакции, если её нет. В этом случае, если метод
registerEntity()
вызывается напрямую, он не создает новую транзакцию. Если метод вызывается изOrganizationService
, то он будет использовать существующую транзакцию, определенную как часть этого класса, в противном случае он не создаст новую транзакцию. -
Propagation.NOT_SUPPORTED. При входе в метод текущая транзакция, если она есть, будет приостановлена и метод будет выполняться без транзакции. В этом случае, если метод
registerEntity()
вызывается напрямую, он не создает новую транзакцию. Если метод вызывается изOrganizationService
, то он не использует существующую транзакцию и не создает свою собственную транзакцию. Он выполняется без транзакции. -
Propagation.REQUIRES_NEW. Танзакция всегда создаётся при входе в метод с этим правилом, ранее созданные транзакции приостанавливаются до момента возврата из метода. Если метод
registerEntity()
вызывается напрямую, он создает новую транзакцию. В то время как, если этот метод вызывается изOrganizationService
, транзакция не будет использовать существующую транзакцию, вызванную на уровне службы, вместо этого она создаст свою собственную новую транзакцию. Если у вызывающей службы не была определена транзакция, она все равно создаст новую транзакцию. -
Propagation.MANDATORY. Обратный по отношению к Propagation.REQUIRES_NEW: всегда используется существующая транзакция и выбрасывается исключение, если текущей транзакции нет. Если метод
registerEntity()
вызывается напрямую, он генерирует исключение. В случае, если метод вызывается изOrganizationService
, метод использует существующую транзакцию. В противном случае он генерирует исключение. -
Propagation.NESTED. Корректно работает только с базами данных, которые умеют savepoints. При входе в метод в уже существующей транзакции создаётся savepoint, который по результатам выполнения метода будет либо сохранён, либо откачен. Все изменения, внесённые методом, подтвердятся только позже, с подтверждением всей транзакции. Если текущей транзакции не существует, будет создана новая. Если транзакция присутствует, Spring проверяет ее и помечает точку сохранения. Это означает, что транзакция возвращается к этой точке сохранения, если при выполнении нашей бизнес-логики возникает проблема. Он работает аналогично
Propagation.REQUIRED
, если нет текущих транзакций. В случаеPropagation.NESTED
вJpaTransactionManager
поддерживаются только соединения JDBC. Однако, если наш драйвер JDBC поддерживает точки сохранения, установка значенияnestedTransactionAllowed
равнымtrue
также позволяет использовать код доступа JDBC в функции транзакций JPA.
Уровни изоляции в транзакциях Sprign
Когда две транзакции одновременно воздействуют на один и тот же объект базы данных, то это состояние базы данных определяется как изоляция транзакций. Оно включает блокировку записей базы данных. Другими словами, он определяет, как будет вести себя база данных или что произойдет, когда одна транзакция обрабатывается с объектом базы данных, а другая параллельная транзакция захочет получить доступ к тому же объекту базы данных или обновить его в то же время.
Одной из характеристик ACID (атомарность, согласованность, изоляция, долговечность) является изолированность. Таким образом, уровень изоляции транзакций не является эксклюзивной функцией платформы Spring. Spring позволяет настроить уровень изоляции в соответствии с нашей бизнес-логикой. Уровень изоляции транзакции устанавливается с помощью параметров аннотации @Transactional
:
@Transactional(isolation = мREAD_UNCOMMITTED)
В Spring есть 5 enum, которые позволяют установить уровень изоляции:
-
Isolation.DEFAULT - Уровень изоляции в Spring по умолчанию равен DEFAULT, что означает, что когда Spring создает новую транзакцию, уровень изоляции будет соответствовать изоляции нашей СУБД по умолчанию. Поэтому мы должны быть осторожны при изменении базы данных.
-
Isolation.READ_UNCOMMITTED - Если две транзакции выполняются одновременно, вторая транзакция может обновить как новые, так и существующие записи до того, как будет зафиксирована первая транзакция. Недавно добавленные и измененные записи отражаются в первой транзакции, которая все еще выполняется, даже если вторая транзакция еще не зафиксирована.
Примечание: PostgreSQL не поддерживает изоляцию
READ_UNCOMMITTED
и вместо этого возвращается кIsolation.READ_COMMITTED
. Кроме того, Oracle не поддерживает и не разрешаетIsolation.READ_UNCOMMITTED
. -
Isolation.SERIALIZABLE - когда две транзакции выполняются одновременно, создается впечатление, что они выполняются последовательно, причем первая транзакция фиксируется до того, как выполняется вторая. Это самый высокий уровень изоляции, который считается полной изоляцией. Таким образом, текущая транзакция становится неуязвимой для воздействия других транзакций. Но из-за низкой производительности и потенциальной взаимоблокировки это может вызвать проблемы.
Обработка ошибок с помощью @Transactional
В аннотации @Transactional
используются атрибуты rollbackFor
или rollbackForClassName
для отката транзакций, а также атрибуты noRollbackFor
или noRollbackForClassName
для предотвращения отката перечисленных исключений.
Согласно документации Spring:
В конфигурации по умолчанию, код инфраструктуры транзакций Spring Framework, помечает транзакцию для отката (rollback) только в случае непроверяемых (unchecked) исключений во время выполнения. То есть, когда генерируемое исключение является экземпляром или подклассом
RuntimeException
. (ЭкземплярыError
также по умолчанию приводят к откату). Проверяемые (Checked) исключения, которые генерируются из транзакционного метода, не приводят к откату в конфигурации по умолчанию.
Таким образом, поведение отката по умолчанию при декларативном подходе приводит к откату (rollback) только исключений во время выполнения (RuntimeException
). То есть, если возникнет проверяемое (checked) исключение, и мы явно не сообщим Spring, что необходимо откатить (rollback) транзакцию, изменения будут зафиксированы в базе.
Откат при возникновении исключения во время выполнения
Давайте рассмотрим случай, когда ожидается, что код выполнит откат при возникновении исключения во время выполнения:
@Transactional
public void rollbackOnRuntimeException() {
jdbcTemplate.execute("insert into sample_table values('abc')");
throw new RuntimeException("Rollback as we have a Runtime Exception!");
}
Spring выполнит откат, когда столкнется с исключением.
Нет отката для проверяемого (checked) исключения
Если мы объявим обычное исключение (Exception) и не объявим стратегию отката, то данные будут вставлены и сохранены в базу данных.
@Transactional
public void noRollbackOnCheckedException() throws Exception {
jdbcTemplate.execute("insert into sample_table values('abc')");
throw new Exception("Generic exception occurred");
}
Откат проверяемого (checked) исключения
Если мы передадим стратегию rollbackFor
для отката ее изменений для пользовательского проверяемого исключения, то она будет откатываться при возникновении исключения:
@Transactional(rollbackFor = CustomCheckedException.class)
public void rollbackOnDeclaredException() throws CustomCheckedException {
jdbcTemplate.execute("insert into sample_table values('abc')");
throw new CustomCheckedException("rollback on checked exception");
}
Откат также будет выполнен, если возникнет какое-либо исключение во время выполнения (RuntimeException
) , в процессе выполнения приведенного выше кода.
Нет отката при RuntimeException
Если мы определим Spring noRollbackFor
в случае RuntimeException
, то код зафиксирует транзакцию, даже если в коде есть какое-либо RuntimeException
исключение:
@Transactional(noRollbackFor = RuntimeException.class)
public void noRollbackOnRuntimeException() {
jdbcTemplate.execute("insert into sample_table values('abc')");
throw new IllegalStateException("Exception");
}
Заключение
В этой статье мы рассмотрели базовую конфигурацию и использование транзакций в экосистеме Spring. Мы также подробно рассмотрели свойства распространения и изоляции @Transactional
. Мы также узнали о различных побочных эффектах и подводных камнях параллелизма аннотации @Transactiona
l.
Автор: ms_shcherbach