Рубрики
Без рубрики

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

Краткое и практическое руководство по транзакциям в Java и Spring.

Автор оригинала: Kumar Chandrakant.

1. введение

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

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

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

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

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

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

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

3.1. JDBC

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

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

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

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();
}

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

3.2. JPA

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

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

Архитектура 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 (Exceotion e) {
    entityManager.getTransaction().rollback();
}

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

3.3. JMS

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

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

JMS позволяет нам создать Сессия от а Соединение которые мы получаем от конкретного поставщика ConnectionFactory . У нас есть возможность создания Сессия это совершается или нет . За неисполнение сделки Сессия s , мы также можем дополнительно определить соответствующий режим подтверждения.

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

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 для Назначения типа темы. Мы получаем Пункт назначения из Сеанса , который мы создали ранее. Далее мы используем Session для определения границ транзакций с помощью методов commit и rollback .

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

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

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

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

4.1. JTA

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

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

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

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

4.2. JTS

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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 предоставляет нам эту бесшовную абстракцию, создавая прокси-сервер для методов с транзакционным кодом. Прокси-сервер управляет состоянием транзакции от имени кода с помощью Диспетчера транзакций : Центральный интерфейс здесь – PlatformTransactionManager , который имеет ряд различных доступных реализаций. Он предоставляет абстракции через JDBC (источник данных), 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 здесь.

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

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

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

@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;
}

Итак, как мы можем видеть, здесь мы оборачиваем JMS-провайдера XAConnectionFactory с AtomikosConnectionFactoryBean .

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

@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;
}

Как и прежде, Источник данных , который мы задаем в 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. Но помните, что вся эта конфигурация позволила нам абстрагироваться от большинства стандартных шаблонов, специфичных для поставщика, и наш фактический код приложения вообще не должен об этом знать.

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

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

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

@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 . Затем мы можем использовать Transactiontemplate для обработки группы операторов в глобальной транзакции.

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

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

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

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

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

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

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

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