Въведение в транзакциите в Java и пролетта

1. Въведение

В този урок ще разберем какво се разбира под транзакции в Java. По този начин ще разберем как да извършваме локални транзакции на ресурси и глобални транзакции. Това също ще ни позволи да проучим различни начини за управление на транзакции в Java и Spring.

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

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

Също така тези транзакции могат да включват един или повече ресурси като база данни, опашка от съобщения, което поражда различни начини за извършване на действия по транзакция. Те включват извършване на локални транзакции на ресурси с отделни ресурси. Алтернативно, множество ресурси могат да участват в глобална транзакция.

3. Ресурсни местни транзакции

Първо ще проучим как можем да използваме транзакции в Java, докато работим с отделни ресурси. Тук може да имаме няколко отделни действия, които изпълняваме с ресурс като база данни . Но може да искаме те да се случат като единно цяло, като в неделима единица работа. С други думи, ние искаме тези действия да се случват при една транзакция.

В Java имаме няколко начина за достъп и работа с ресурс като база данни. Следователно начинът, по който се справяме със сделките, също не е еднакъв. В този раздел ще открием как можем да използваме транзакции с някои от тези библиотеки в Java, които са доста често използвани.

3.1. JDBC

Java Database Connectivity (JDBC) е API в Java, който определя как да се осъществява достъп до бази данни в Java . Различните доставчици на бази данни осигуряват JDBC драйвери за свързване към базата данни по начин, агностичен от доставчика. И така, извличаме връзка от драйвер за извършване на различни операции в базата данни:

JDBC ни предоставя опциите за изпълнение на извлечения по транзакция. В поведението по подразбиране на Connection е авто-ангажират . За да се изясни, това означава, че всеки отделен извлечение се третира като транзакция и автоматично се ангажира веднага след изпълнението.

Ако обаче искаме да обединим множество извлечения в една транзакция, това е възможно да се постигне и:

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 също ни позволява да зададем Savepoint, която ни предоставя по-голям контрол върху това колко да се върне.

3.2. JPA

API за устойчивост на Java (JPA) е спецификация в Java, която може да се използва за преодоляване на пропастта между обектно-ориентирани модели на домейни и релационни системи от бази данни . И така, има няколко реализации на JPA, достъпни от трети страни като Hibernate, EclipseLink и iBatis.

В JPA можем да определим редовните класове като Обект, който им осигурява постоянна идентичност. Класът EntityManager осигурява необходимия интерфейс за работа с множество обекти в контекста на постоянство . Контекстът на постоянство може да се разглежда като кеш от първо ниво, където се управляват обекти:

Контекстът на постоянство тук може да бъде два вида, с обхват на транзакция или с разширен обхват. Контекстът на постоянство с обхват на транзакция е обвързан с една транзакция. Докато контекстът на устойчивост с разширен обхват може да обхваща множество транзакции. Обхватът по подразбиране на контекста на постоянство е обхват на транзакцията .

Нека видим как можем да създадем 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 в контекста на контекст на трайност с обхват от транзакции. Тогава ние сме определяне края на транзакцията с започнете , се ангажират, и намаление на цените методи.

3.3. JMS

Java Messaging Service (JMS) е спецификация в Java, която позволява на приложенията да комуникират асинхронно, използвайки съобщения . API ни позволява да създаваме, изпращаме, получаваме и четем съобщения от опашка или тема. Има няколко услуги за съобщения, които отговарят на спецификациите на JMS, включително OpenMQ и ActiveMQ.

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

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 за местоназначението на типа тема. Получаваме дестинацията от сесията, която създадохме по-рано. Освен това използваме Session, за да дефинираме границите на транзакциите, използвайки методите за ангажиране и връщане назад .

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

Както видяхме, локалните транзакции на ресурси ни позволяват да изпълняваме множество операции в рамките на един ресурс като единно цяло. Но доста често имаме работа с операции, които обхващат множество ресурси . Например работа в две различни бази данни или база данни и опашка за съобщения. Тук подкрепата за локални транзакции в рамките на ресурсите няма да ни бъде достатъчна.

Това, от което се нуждаем в тези сценарии, е глобален механизъм за разграничаване на транзакции, обхващащи множество участващи ресурси . Това често е известно като разпределени транзакции и има спецификации, които са предложени за ефективно справяне с тях.

Най- XA спецификация е една такава спецификация, която дефинира мениджър на транзакции за контрол сделка в множество ресурси . Java има доста зряла поддръжка за разпределени транзакции, отговарящи на спецификацията XA, чрез компонентите JTA и JTS.

4.1. JTA

Java Transaction API (JTA) is a Java Enterprise Edition API developed under the Java Community Process. It enables Java applications and application servers to perform distributed transactions across XA resources. JTA is modeled around XA architecture, leveraging two-phase commit.

JTA specifies standard Java interfaces between a transaction manager and the other parties in a distributed transaction:

Let's understand some of the key interfaces highlighted above:

  • TransactionManager: An interface which allows an application server to demarcate and control transactions
  • UserTransaction: This interface allows an application program to demarcate and control transactions explicitly
  • XAResource: The purpose of this interface is to allow a transaction manager to work with resource managers for XA-compliant resources

4.2. JTS

Java Transaction Service (JTS) is a specification for building the transaction manager that maps to the OMG OTS specification. JTS uses the standard CORBA ORB/TS interfaces and Internet Inter-ORB Protocol (IIOP) for transaction context propagation between JTS transaction managers.

At a high level, it supports the Java Transaction API (JTA). A JTS transaction manager provides transaction services to the parties involved in a distributed transaction:

Services that JTS provides to an application are largely transparent and hence we may not even notice them in the application architecture. JTS is architected around an application server which abstracts all transaction semantics from the application programs.

5. JTA Transaction Management

Now it's time to understand how we can manage a distributed transaction using JTA. Distributed transactions are not trivial solutions and hence have cost implications as well. Moreover, there are multiple options that we can choose from to include JTA in our application. Hence, our choice must be in the view of overall application architecture and aspirations.

5.1. JTA in Application Server

As we have seen earlier, JTA architecture relies on the application server to facilitate a number of transaction-related operations. One of the key services it relies on the server to provide is a naming service through JNDI. This is where XA resources like data sources are bound to and retrieved from.

Apart from this, we have a choice in terms of how we want to manage the transaction boundary in our application. This gives rise to two types of transactions within the Java application server:

  • Container-managed Transaction: As the name suggests, here the transaction boundary is set by the application server. This simplifies the development of Enterprise Java Beans (EJB) as it does not include statements related to transaction demarcation and relies solely on the container to do so. However, this does not provide enough flexibility for the application.
  • Bean-managed Transaction: Contrary to the container-managed transaction, in a bean-managed transaction EJBs contain the explicit statements to define the transaction demarcation. This provides precise control to the application in marking the boundaries of the transaction, albeit at the cost of more complexity.

One of the main drawbacks of performing transactions in the context of an application server is that the application becomes tightly coupled with the server. This has implications with respect to testability, manageability, and portability of the application. This is more profound in microservice architecture where the emphasis is more on developing server-neutral applications.

5.2. JTA Standalone

The problems we discussed in the last section have provided a huge momentum towards creating solutions for distributed transactions that does not rely on an application server. There are several options available to us in this regard, like using transaction support with Spring or use a transaction manager like Atomikos.

Let's see how we can use a transaction manager like Atomikos to facilitate a distributed transaction with a database and a message queue. One of the key aspects of a distributed transaction is enlisting and delisting the participating resources with the transaction monitor. Atomikos takes care of this for us. All we have to do is use Atomikos-provided abstractions:

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

Here, we are creating an instance of AtomikosDataSourceBean and registering the vendor-specific XADataSource. From here on, we can continue using this like any other DataSource and get the benefits of distributed transactions.

Similarly, we have an abstraction for message queue which takes care of registering the vendor-specific XA resource with the transaction monitor automatically:

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

Here, we are creating an instance of AtomikosConnectionFactoryBean and registering the XAConnectionFactory from an XA-enabled JMS vendor. After this, we can continue to use this as a regular ConnectionFactory.

Now, Atomikos provides us the last piece of the puzzle to bring everything together, an instance of UserTransaction:

UserTransaction userTransaction = new UserTransactionImp();

Now, we are ready to create an application with distributed transaction spanning across our database and the message queue:

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

Here, we are using the methods begin and commit in the class UserTransaction to demarcate the transaction boundary. This includes saving a record in the database as well as publishing a message to the message queue.

6. Transactions Support in Spring

We have seen that handling transactions are rather an involved task which includes a lot of boilerplate coding and configurations. Moreover, each resource has its own way of handling local transactions. In Java, JTA abstracts us from these variations but further brings provider-specific details and the complexity of the application server.

Spring platform provides us a much cleaner way of handling transactions, both resource local and global transactions in Java. This together with the other benefits of Spring creates a compelling case for using Spring to handle transactions. Moreover, it's quite easy to configure and switch a transaction manager with Spring, which can be server provided or standalone.

Spring provides us this seamless abstraction by creating a proxy for the methods with transactional code. The proxy manages the transaction state on behalf of the code with the help of TransactionManager:

The central interface here is PlatformTransactionManager which has a number of different implementations available. It provides abstractions over JDBC (DataSource), JMS, JPA, JTA, and many other resources.

6.1. Configurations

Let's see how we can configure Spring to use Atomikos as a transaction manager and provide transactional support for JPA and JMS. We'll begin by defining a PlatformTransactionManager of the type JTA:

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

Here, we are providing instances of UserTransaction and TransactionManager to JTATransactionManager. These instances are provided by a transaction manager library like Atomikos:

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

The classes UserTransactionImp and UserTransactionManager are provided by Atomikos here.

Further, we need to define the JmsTemplete which the core class allowing synchronous JMS access in Spring:

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

Here, ConnectionFactory is provided by Atomikos where it enables distributed transaction for Connection provided by it:

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

So, as we can see, here we are wrapping a JMS provider-specific XAConnectionFactory with AtomikosConnectionFactoryBean.

Next, we need to define an AbstractEntityManagerFactoryBean that is responsible for creating JPA EntityManagerFactory bean in 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; }

As before, the DataSource that we set in the LocalContainerEntityManagerFactoryBean here is provided by Atomikos with distributed transactions enabled:

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

Here again, we are wrapping the provider-specific XADataSource in AtomikosDataSourceBean.

6.2. Transaction Management

Having gone through all the configurations in the last section, we must feel quite overwhelmed! We may even question the benefits of using Spring after all. But do remember that all this configuration has enabled us abstraction from most of the provider-specific boilerplate and our actual application code does not need to be aware of that at all.

So, now we are ready to explore how to use transactions in Spring where we intend to update the database and publish messages. Spring provides us two ways to achieve this with their own benefits to choose from. Let's understand how we can make use of them:

  • Declarative Support

The easiest way to use transactions in Spring is with declarative support. Here, we have a convenience annotation available to be applied at the method or even at the class. This simply enables global transaction for our code:

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

The simple code above is sufficient to allow a save-operation in the database and a publish-operation in message queue within a JTA transaction.

  • Programmatic Support

While the declarative support is quite elegant and simple, it does not offer us the benefit of controlling the transaction boundary more precisely. Hence, if we do have a certain need to achieve that, Spring offers programmatic support to demarcate transaction boundary:

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

So, as we can see, we have to create a TransactionTemplate with the available PlatformTransactionManager. Then we can use the TransactionTemplete to process a bunch of statements within a global transaction.

7. Afterthoughts

As we have seen that handling transactions, particularly those that span across multiple resources are complex. Moreover, transactions are inherently blocking which is detrimental to latency and throughput of an application. Further, testing and maintaining code with distributed transactions is not easy, especially if the transaction depends on the underlying application server. So, all in all, it's best to avoid transactions at all if we can!

But that is far from reality. In short, in real-world applications, we do often have a legitimate need for transactions. Although it's possible to rethink the application architecture without transactions, it may not always be possible. Hence, we must adopt certain best practices when working with transactions in Java to make our applications better:

  • One of the fundamental shifts we should adopt is to use standalone transaction managers instead of those provided by an application server. This alone can simplify our application greatly. Moreover, it's much suited for cloud-native microservice architecture.
  • Further, an abstraction layer like Spring can help us contain the direct impact of providers like JPA or JTA providers. So, this can enable us to switch between providers without much impact on our business logic. Moreover, it takes away the low-level responsibilities of managing the transaction state from us.
  • Lastly, we should be careful in picking the transaction boundary in our code. Since transactions are blocking, it's always better to keep the transaction boundary as restricted as possible. If necessary we should prefer programmatic over declarative control for transactions.

8. Conclusion

В обобщение, в този урок обсъдихме транзакциите в контекста на Java. Преминахме през поддръжка на отделни ресурсни локални транзакции в Java за различни ресурси. Преминахме и през начините за постигане на глобални транзакции в Java.

Освен това преминахме през различни начини за управление на глобални транзакции в Java. Също така разбрахме как Spring улеснява използването на транзакции в Java.

И накрая, преминахме през някои от най-добрите практики при работа с транзакции в Java.