Перейти к основному содержимому

Введение в транзакции в Java и Spring

· 14 мин. чтения

1. Введение

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

2. Что такое транзакция?

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

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

3. Локальные транзакции ресурсов

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

В Java у нас есть несколько способов доступа и работы с таким ресурсом, как база данных. Следовательно, то, как мы работаем с транзакциями, тоже не то же самое. В этом разделе мы узнаем, как мы можем использовать транзакции с некоторыми из этих библиотек в Java, которые довольно часто используются.

3.1. JDBC

Java Database Connectivity (JDBC)это API в Java, который определяет, как получить доступ к базам данных в Java . Различные поставщики баз данных предоставляют драйверы JDBC для подключения к базе данных независимо от поставщика. Итак, мы получаем Connection от драйвера для выполнения различных операций с базой данных:

./7bbf3bf83b943073da74e82a7d5e59f8.jpg

JDBC предоставляет нам возможность выполнять операторы в рамках транзакции. Поведение Connection по умолчанию — auto- commit . Чтобы уточнить, это означает, что каждый отдельный оператор рассматривается как транзакция и автоматически фиксируется сразу после выполнения.

Однако, если мы хотим объединить несколько операторов в одну транзакцию, этого также можно добиться:

Connection connection = DriverManager.getConnection(CONNECTION_URL, USER, PASSWORD);
try {
connection.setAutoCommit(false);
PreparedStatement firstStatement = connection .prepareStatement("firstQuery");
firstStatement.executeUpdate();
PreparedStatement secondStatement = connection .prepareStatement("secondQuery");
secondStatement.executeUpdate();
connection.commit();
} catch (Exception e) {
connection.rollback();
}

Здесь мы отключили режим автоматической фиксации Connection . Следовательно, мы можем вручную определить границу транзакции и выполнить фиксацию или откат . JDBC также позволяет нам установить точку сохранения , которая дает нам больше контроля над тем, сколько нужно откатить.

3.2. JPA

Java Persistence API (JPA) — это спецификация Java, которую можно использовать для преодоления разрыва между объектно-ориентированными моделями предметной области и системами реляционных баз данных . Таким образом, есть несколько реализаций JPA, доступных от третьих сторон, таких как Hibernate, EclipseLink и iBatis.

В JPA мы можем определить обычные классы как Entity , которые предоставляют им постоянную идентичность. Класс EntityManager предоставляет необходимый интерфейс для работы с несколькими сущностями в контексте постоянства . Контекст персистентности можно рассматривать как кеш первого уровня, в котором осуществляется управление сущностями:

./628a76eadb95e4e37aeb9df32f034484.jpg

JPA-архитектура

Контекст персистентности здесь может быть двух типов: с областью действия транзакции или с расширенной областью действия. Контекст сохраняемости в области транзакции привязан к одной транзакции. В то время как контекст сохраняемости с расширенной областью действия может охватывать несколько транзакций. Областью действия по умолчанию для контекста сохраняемости является транзакция-область .

Давайте посмотрим, как мы можем создать EntityManager и определить границу транзакции вручную:

EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("jpa-example");
EntityManager entityManager = entityManagerFactory.createEntityManager();
try {
entityManager.getTransaction().begin();
entityManager.persist(firstEntity);
entityManager.persist(secondEntity);
entityManager.getTransaction().commit();
} catch (Exception e) {
entityManager.getTransaction().rollback();
}

Здесь мы создаем EntityManager из EntityManagerFactory в контексте контекста сохраняемости в области транзакции. Затем мы определяем границу транзакции с помощью методов begin , commit и rollback .

3.3. JMS

Служба обмена сообщениями Java (JMS) — это спецификация Java, позволяющая приложениям асинхронно взаимодействовать с помощью сообщений . API позволяет нам создавать, отправлять, получать и читать сообщения из очереди или темы. Существует несколько служб обмена сообщениями, которые соответствуют спецификациям JMS, включая OpenMQ и ActiveMQ.

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

./abfb3982a5d0a872c793b0cc3e89102f.jpg

JMS позволяет нам создать сеанс из соединения , которое мы получаем от поставщика ConnectionFactory . У нас есть возможность создать сессию , которая является транзакционной или нет . Для сеансов без транзакций мы также можем дополнительно определить соответствующий режим подтверждения.

Давайте посмотрим, как мы можем создать сеанс транзакции для отправки нескольких сообщений в рамках транзакции:

ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(CONNECTION_URL);
Connection connection = = connectionFactory.createConnection();
connection.start();
try {
Session session = connection.createSession(true, 0);
Destination = destination = session.createTopic("TEST.FOO");
MessageProducer producer = session.createProducer(destination);
producer.send(firstMessage);
producer.send(secondMessage);
session.commit();
} catch (Exception e) {
session.rollback();
}

Здесь мы создаем MessageProducer для пункта назначения типа темы. Мы получаем Destination из сеанса , который мы создали ранее. Далее мы используем Session для определения границ транзакций с помощью методов commit и rollback .

4. Глобальные транзакции

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

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

Спецификация XA является одной из таких спецификаций, которая определяет диспетчер транзакций для управления транзакциями на нескольких ресурсах . Java имеет довольно зрелую поддержку распределенных транзакций, соответствующих спецификации XA, через компоненты JTA и JTS.

4.1. JTA

Java Transaction API (JTA) — это API Java Enterprise Edition, разработанный в рамках Java Community Process. Он позволяет Java-приложениям и серверам приложений выполнять распределенные транзакции по ресурсам XA . JTA смоделирован на основе архитектуры XA с использованием двухэтапной фиксации.

JTA определяет стандартные интерфейсы Java между менеджером транзакций и другими сторонами распределенной транзакции:

./3c7ca959a4b7d93b69b762967b6982bf.png

Давайте разберемся с некоторыми из ключевых интерфейсов, выделенных выше:

  • TransactionManager : интерфейс, который позволяет серверу приложений разграничивать и контролировать транзакции .
  • UserTransaction : этот интерфейс позволяет прикладной программе явно разграничивать и контролировать транзакции .
  • XAResource : цель этого интерфейса — позволить диспетчеру транзакций работать с диспетчерами ресурсов для XA-совместимых ресурсов.

4.2. СТС

Служба транзакций Java (JTS)это спецификация для построения диспетчера транзакций, которая соответствует спецификации OMG OTS . JTS использует стандартные интерфейсы CORBA ORB/TS и Интернет-протокол Inter-ORB (IIOP) для распространения контекста транзакции между менеджерами транзакций JTS.

На высоком уровне он поддерживает Java Transaction API (JTA). Менеджер транзакций JTS предоставляет транзакционные услуги сторонам, участвующим в распределенной транзакции:

./7d087bc32e47493eda702145b2e8cafb.png

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

5. Управление транзакциями JTA

Теперь пришло время понять, как мы можем управлять распределенной транзакцией с помощью JTA. Распределенные транзакции не являются тривиальными решениями и, следовательно, также влекут за собой затраты. Более того, есть несколько вариантов включения JTA в наше приложение . Следовательно, наш выбор должен основываться на общей архитектуре приложения и стремлениях.

5.1. JTA на сервере приложений

Как мы видели ранее, архитектура JTA опирается на сервер приложений для облегчения ряда операций, связанных с транзакциями . Одна из ключевых служб, которую сервер должен предоставлять, — это служба именования через JNDI. Именно здесь ресурсы XA, такие как источники данных, привязываются и откуда извлекаются.

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

  • Транзакция, управляемая контейнером : как следует из названия, здесь граница транзакции устанавливается сервером приложений . Это упрощает разработку Enterprise Java Beans (EJB), поскольку он не включает операторов, связанных с разграничением транзакций, и для этого полагается исключительно на контейнер. Однако это не обеспечивает достаточной гибкости для приложения.
  • Транзакция , управляемая компонентом: в отличие от транзакции, управляемой контейнером, EJB-компоненты транзакции, управляемой компонентом, содержат явные операторы для определения границы транзакции . Это обеспечивает точное управление приложением при обозначении границ транзакции, хотя и за счет большей сложности.

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

5.2. Автономный JTA

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

Давайте посмотрим, как мы можем использовать диспетчер транзакций, такой как Atomikos , для облегчения распределенной транзакции с базой данных и очередью сообщений. Одним из ключевых аспектов распределенной транзакции является зачисление и удаление участвующих ресурсов с помощью монитора транзакций . Атомикос позаботится об этом за нас. Все, что нам нужно сделать, это использовать абстракции, предоставленные Atomikos:

AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();
atomikosDataSourceBean.setXaDataSourceClassName("com.mysql.cj.jdbc.MysqlXADataSource");
DataSource dataSource = atomikosDataSourceBean;

Здесь мы создаем экземпляр AtomikosDataSourceBean и регистрируем специфичный для поставщика XADataSource . С этого момента мы можем продолжать использовать его, как и любой другой источник данных , и получать преимущества распределенных транзакций.

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

AtomikosConnectionFactoryBean atomikosConnectionFactoryBean = new AtomikosConnectionFactoryBean();
atomikosConnectionFactoryBean.setXaConnectionFactory(new ActiveMQXAConnectionFactory());
ConnectionFactory connectionFactory = atomikosConnectionFactoryBean;

Здесь мы создаем экземпляр AtomikosConnectionFactoryBean и регистрируем XAConnectionFactory от поставщика JMS с поддержкой XA. После этого мы можем продолжать использовать это как обычную ConnectionFactory .

Теперь Atomikos предоставляет нам последнюю часть головоломки, чтобы собрать все вместе, экземпляр UserTransaction :

UserTransaction userTransaction = new UserTransactionImp();

Теперь мы готовы создать приложение с распределенной транзакцией, охватывающей нашу базу данных и очередь сообщений:

try {
userTransaction.begin();

java.sql.Connection dbConnection = dataSource.getConnection();
PreparedStatement preparedStatement = dbConnection.prepareStatement(SQL_INSERT);
preparedStatement.executeUpdate();

javax.jms.Connection mbConnection = connectionFactory.createConnection();
Session session = mbConnection.createSession(true, 0);
Destination destination = session.createTopic("TEST.FOO");
MessageProducer producer = session.createProducer(destination);
producer.send(MESSAGE);

userTransaction.commit();
} catch (Exception e) {
userTransaction.rollback();
}

Здесь мы используем методы begin и commit в классе UserTransaction , чтобы разграничить границу транзакции . Сюда входит сохранение записи в базе данных, а также публикация сообщения в очереди сообщений.

6. Поддержка транзакций весной

Мы видели, что обработка транзакций — довольно сложная задача, которая включает в себя множество стандартных кодов и конфигураций. Более того, у каждого ресурса есть свой способ обработки локальных транзакций. В Java JTA абстрагирует нас от этих вариаций, но вносит дополнительные детали, специфичные для провайдера, и сложность сервера приложений.

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

Spring предоставляет нам эту бесшовную абстракцию, создавая прокси для методов с транзакционным кодом. Прокси управляет состоянием транзакции от имени кода с помощью TransactionManager :

./f9f6a0537cac71b8aeef086ffd70bbbd.jpg

центральным интерфейсом здесь является PlatformTransactionManager , который имеет ряд доступных реализаций. Он предоставляет абстракции над JDBC (DataSource), JMS, JPA, JTA и многими другими ресурсами.

6.1. Конфигурации

Давайте посмотрим, как мы можем настроить Spring для использования Atomikos в качестве менеджера транзакций и обеспечения поддержки транзакций для JPA и JMS . Мы начнем с определения PlatformTransactionManager типа JTA:

@Bean
public PlatformTransactionManager platformTransactionManager() throws Throwable {
return new JtaTransactionManager(
userTransaction(), transactionManager());
}

Здесь мы предоставляем экземпляры UserTransaction и TransactionManager для JTATransactionManager . Эти экземпляры предоставляются библиотекой менеджера транзакций, такой как Atomikos:

@Bean
public UserTransaction userTransaction() {
return new UserTransactionImp();
}

@Bean(initMethod = "init", destroyMethod = "close")
public TransactionManager transactionManager() {
return new UserTransactionManager();
}

Классы UserTransactionImp и UserTransactionManager предоставляются Atomikos здесь.

Кроме того, нам нужно определить JmsTemplete , который является основным классом, обеспечивающим синхронный доступ к JMS в Spring:

@Bean
public JmsTemplate jmsTemplate() throws Throwable {
return new JmsTemplate(connectionFactory());
}

Здесь ConnectionFactory предоставляется Atomikos, где он включает распределенную транзакцию для предоставленного им Connection :

@Bean(initMethod = "init", destroyMethod = "close")
public ConnectionFactory connectionFactory() {
ActiveMQXAConnectionFactory activeMQXAConnectionFactory = new
ActiveMQXAConnectionFactory();
activeMQXAConnectionFactory.setBrokerURL("tcp://localhost:61616");
AtomikosConnectionFactoryBean atomikosConnectionFactoryBean = new AtomikosConnectionFactoryBean();
atomikosConnectionFactoryBean.setUniqueResourceName("xamq");
atomikosConnectionFactoryBean.setLocalTransactionMode(false);
atomikosConnectionFactoryBean.setXaConnectionFactory(activeMQXAConnectionFactory);
return atomikosConnectionFactoryBean;
}

Итак, как мы видим, здесь мы оборачиваем XAConnectionFactory, зависящую от провайдера JMS , в AtomikosConnectionFactoryBean . ``

Далее нам нужно определить AbstractEntityManagerFactoryBean , который отвечает за создание JPA -компонента EntityManagerFactory в Spring:

@Bean
public LocalContainerEntityManagerFactoryBean entityManager() throws SQLException {
LocalContainerEntityManagerFactoryBean entityManager = new LocalContainerEntityManagerFactoryBean();
entityManager.setDataSource(dataSource());
Properties properties = new Properties();
properties.setProperty( "javax.persistence.transactionType", "jta");
entityManager.setJpaProperties(properties);
return entityManager;
}

Как и прежде, DataSource , который мы установили в LocalContainerEntityManagerFactoryBean здесь, предоставляется Atomikos с включенными распределенными транзакциями:

@Bean(initMethod = "init", destroyMethod = "close")
public DataSource dataSource() throws SQLException {
MysqlXADataSource mysqlXaDataSource = new MysqlXADataSource();
mysqlXaDataSource.setUrl("jdbc:mysql://127.0.0.1:3306/test");
AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
xaDataSource.setXaDataSource(mysqlXaDataSource);
xaDataSource.setUniqueResourceName("xads");
return xaDataSource;
}

Здесь мы снова оборачиваем XADataSource, специфичный для поставщика , в AtomikosDataSourceBean . ``

6.2. Управление транзакциями

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

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

  • Декларативная поддержка

Самый простой способ использования транзакций в Spring — это декларативная поддержка. Здесь у нас есть удобная аннотация, которую можно применить к методу или даже к классу . Это просто включает глобальную транзакцию для нашего кода:

@PersistenceContext
EntityManager entityManager;

@Autowired
JmsTemplate jmsTemplate;

@Transactional(propagation = Propagation.REQUIRED)
public void process(ENTITY, MESSAGE) {
entityManager.persist(ENTITY);
jmsTemplate.convertAndSend(DESTINATION, MESSAGE);
}

Приведенного выше простого кода достаточно, чтобы разрешить операцию сохранения в базе данных и операцию публикации в очереди сообщений в транзакции JTA.

  • Программная поддержка

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

@Autowired
private PlatformTransactionManager transactionManager;

public void process(ENTITY, MESSAGE) {
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
transactionTemplate.executeWithoutResult(status -> {
entityManager.persist(ENTITY);
jmsTemplate.convertAndSend(DESTINATION, MESSAGE);
});
}

Итак, как мы видим, нам нужно создать TransactionTemplate с доступным PlatformTransactionManager . Затем мы можем использовать TransactionTemplete для обработки набора операторов в рамках глобальной транзакции.

7. Запоздалые мысли

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

Но это далеко от реальности. Короче говоря, в реальных приложениях у нас часто возникает законная потребность в транзакциях. Хотя можно переосмыслить архитектуру приложения без транзакций , это не всегда возможно. Следовательно, мы должны использовать некоторые передовые методы работы с транзакциями в Java, чтобы сделать наши приложения лучше:

  • Одним из фундаментальных изменений, которые мы должны принять, является использование автономных менеджеров транзакций вместо тех, что предоставляются сервером приложений . Уже одно это может значительно упростить наше приложение. Кроме того, он очень подходит для облачной микросервисной архитектуры.
  • Кроме того, уровень абстракции, такой как Spring, может помочь нам ограничить прямое влияние поставщиков, таких как поставщики JPA или JTA. Таким образом, это может позволить нам переключаться между поставщиками без особого влияния на нашу бизнес-логику. Более того, это снимает с нас низкоуровневые обязанности по управлению состоянием транзакции.
  • Наконец, мы должны быть осторожны при выборе границы транзакции в нашем коде . Поскольку транзакции блокируются, всегда лучше максимально ограничивать границы транзакций. При необходимости мы должны предпочесть программный контроль транзакциям декларативному.

8. Заключение

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

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

Наконец, мы рассмотрели некоторые из лучших практик при работе с транзакциями в Java.