Un guide sur Atomikos

1. Introduction

Atomikos est une bibliothèque de transactions pour les applications Java . Dans ce tutoriel, nous comprendrons pourquoi et comment utiliser Atomikos.

Dans le processus, nous allons également passer en revue les bases des transactions et pourquoi nous en avons besoin.

Ensuite, nous créerons une application simple avec des transactions exploitant différentes API d'Atomikos.

2. Comprendre les bases

Avant de discuter d'Atomikos, comprenons ce que sont exactement les transactions et quelques concepts qui y sont liés. En termes simples, une transaction est une unité logique de travail dont l'effet est visible à l'extérieur de la transaction soit dans son intégralité, soit pas du tout .

Prenons un exemple pour mieux comprendre cela. Une application de vente au détail typique réserve l'inventaire et passe ensuite une commande:

Ici, nous aimerions que ces deux opérations se produisent ensemble ou ne se produisent pas du tout. Nous pouvons y parvenir en regroupant ces opérations en une seule transaction.

2.1. Transaction locale ou distribuée

Une transaction peut impliquer plusieurs opérations indépendantes. Ces opérations peuvent s'exécuter sur la même ressource ou sur des ressources différentes . Nous nous référons aux composants participant à une transaction comme une base de données en tant que ressource ici.

Les transactions au sein d'une seule ressource sont des transactions locales connues, tandis que celles générées sur plusieurs ressources sont appelées transaction distribuée:

Ici, l'inventaire et les commandes peuvent être deux tables dans la même base de données, ou ils peuvent être deux bases de données différentes - pouvant être exécutées sur des machines différentes.

2.2. Spécification XA et API de transaction Java

XA fait référence à eXtended Architecture, qui est une spécification pour le traitement des transactions distribuées. Le but de XA est de fournir l'atomicité dans les transactions globales impliquant des composants hétérogènes .

La spécification XA assure l'intégrité via un protocole connu sous le nom de validation en deux phases. La validation en deux phases est un algorithme distribué largement utilisé pour faciliter la décision de valider ou d'annuler une transaction distribuée.

Java Transaction API (JTA) est une API Java Enterprise Edition développée dans le cadre du Java Community Process. Il permet aux applications Java et aux serveurs d'applications d'effectuer des transactions distribuées sur les ressources XA .

JTA est modélisé autour de l'architecture XA, tirant parti de la validation en deux phases. JTA spécifie les interfaces Java standard entre un gestionnaire de transactions et les autres parties dans une transaction distribuée.

3. Introduction à Atomikos

Maintenant que nous avons passé en revue les bases des transactions, nous sommes prêts à apprendre Atomikos. Dans cette section, nous comprendrons ce qu'est exactement Atomikos et comment il se rapporte à des concepts tels que XA et JTA. Nous allons également comprendre l'architecture d'Atomikos et parcourir ses offres de produits.

3.1. Qu'est-ce que Atomikos

Comme nous l'avons vu, JTA fournit des interfaces en Java pour créer des applications avec des transactions distribuées. Maintenant, JTA n'est qu'une spécification et n'offre aucune implémentation. Pour que nous puissions exécuter une application dans laquelle nous exploitons JTA, nous avons besoin d'une implémentation de JTA . Une telle implémentation est appelée un gestionnaire de transactions.

En règle générale, le serveur d'applications fournit une implémentation par défaut du gestionnaire de transactions. Par exemple, dans le cas des Enterprise Java Beans (EJB), les conteneurs EJB gèrent le comportement des transactions sans aucune intervention explicite des développeurs d'applications. Cependant, dans de nombreux cas, cela peut ne pas être idéal, et nous pouvons avoir besoin d'un contrôle direct sur la transaction indépendamment du serveur d'application.

Atomikos est un gestionnaire de transactions léger pour Java qui permet aux applications utilisant des transactions distribuées d'être autonomes. Essentiellement, notre application n'a pas besoin de s'appuyer sur un composant lourd comme un serveur d'applications pour les transactions. Cela rapproche le concept de transactions distribuées d'une architecture cloud native.

3.2. Architecture Atomikos

Atomikos est principalement construit comme un gestionnaire de transactions JTA et, par conséquent, implémente l'architecture XA avec un protocole de validation en deux phases . Voyons une architecture de haut niveau avec Atomikos:

Ici, Atomikos facilite une transaction basée sur la validation en deux phases couvrant une base de données et une file d'attente de messages.

3.3. Offres de produits Atomikos

Atomikos est un gestionnaire de transactions distribué qui offre plus de fonctionnalités que ce que JTA / XA exige. Il dispose d'un produit open-source et d'une offre commerciale beaucoup plus complète:

  • TransactionsEssentials: le produit open source d' Atomikos fournissant un gestionnaire de transactions JTA / XA pour les applications Java fonctionnant avec des bases de données et des files d'attente de messages. Ceci est surtout utile à des fins de test et d'évaluation.
  • ExtremeTransactions: l' offre commerciale d'Atomikos, qui propose des transactions distribuées sur des applications composites, y compris des services REST en dehors des bases de données et des files d'attente de messages. Ceci est utile pour créer des applications exécutant Extreme Transaction Processing (XTP).

Dans ce didacticiel, nous utiliserons la bibliothèque TransactionsEssentials pour créer et démontrer les capacités d'Atomikos.

4. Configuration d'Atomikos

Comme nous l'avons vu précédemment, l'un des points forts d'Atomikos est qu'il s'agit d'un service de transaction intégré . Cela signifie que nous pouvons l'exécuter dans la même JVM que notre application. Ainsi, la configuration d'Atomikos est assez simple.

4.1. Dépendances

Tout d'abord, nous devons configurer les dépendances. Ici, tout ce que nous avons à faire est de déclarer les dépendances dans notre fichier Maven pom.xml :

 com.atomikos transactions-jdbc 5.0.6   com.atomikos transactions-jms 5.0.6 

Nous utilisons des dépendances Atomikos pour JDBC et JMS dans ce cas, mais des dépendances similaires sont disponibles sur Maven Central pour d'autres ressources XA.

4.2. Configurations

Atomikos provides several configuration parameters, with sensible defaults for each of them. The easiest way to override these parameters is to provide a transactions.properties file in the classpath. We can add several parameters for the initialization and operation of the transaction service. Let's see a simple configuration to override the directory where log files are created:

com.atomikos.icatch.file=path_to_your_file

Similarly, there are other parameters that we can use to control the timeout for transactions, set unique names for our application, or define shutdown behavior.

4.3. Databases

In our tutorial, we'll build a simple retail application, like the one we described earlier, which reserves inventory and then places an order. We'll use a relational database for simplicity. Moreover, we'll use multiple databases to demonstrate distributed transactions. However, this can very well extend to other XA-complaint resources like message queues and topics.

Our inventory database will have a simple table to host product inventories:

CREATE TABLE INVENTORY ( productId VARCHAR PRIMARY KEY, balance INT );

And, our order database will have a simple table to host placed orders:

CREATE TABLE ORDERS ( orderId VARCHAR PRIMARY KEY, productId VARCHAR, amount INT NOT NULL CHECK (amount <= 5) );

This is a very basic database schema and useful only for the demonstration. However, it's important to note that our schema constraint does not allow order with a product quantity of more than five.

5. Working With Atomikos

Now, we're ready to use one of the Atomikos libraries to build our application with distributed transactions. In the following subsections, we'll use the built-in Atomikos resource adapters to connect with our back-end database systems. This is the quickest and easiest way to get started with Atomikos.

5.1. Instantiating UserTransaction

We will leverage JTA UserTransaction to demarcate transaction boundaries. All other steps related to transaction service will be automatically taken care of. This includes enlisting and delisting resources with the transaction service.

Firstly, we need to instantiate a UserTransaction from Atomikos:

UserTransactionImp utx = new UserTransactionImp();

5.2. Instantiating DataSource

Then, we need to instantiate a DataSource from Atomikos. There are two versions of DataSource that Atomikos makes available.

The first, AtomikosDataSourceBean, is aware of an underlying XADataSource:

AtomikosDataSourceBean dataSource = new AtomikosDataSourceBean();

While AtomikosNonXADataSourceBean uses any regular JDBC driver class:

AtomikosNonXADataSourceBean dataSource = new AtomikosNonXADataSourceBean();

As the name suggests, AtomikosNonXADataSource is not XA compliant. Hence transactions executed with such a data source can not be guaranteed to be atomic. So why would we ever use this? We may have some database that does not support XA specification. Atomikos does not prohibit us from using such a data source and still try to provide atomicity if there is a single such data source in the transaction. This technique is similar to Last Resource Gambit, a variation of the two-phase commit process.

Further, we need to appropriately configure the DataSource depending upon the database and driver.

5.3. Performing Database Operations

Once configured, it's fairly easy to use DataSource within the context of a transaction in our application:

public void placeOrder(String productId, int amount) throws Exception { String orderId = UUID.randomUUID().toString(); boolean rollback = false; try { utx.begin(); Connection inventoryConnection = inventoryDataSource.getConnection(); Connection orderConnection = orderDataSource.getConnection(); Statement s1 = inventoryConnection.createStatement(); String q1 = "update Inventory set balance = balance - " + amount + " where productId ='" + productId + "'"; s1.executeUpdate(q1); s1.close(); Statement s2 = orderConnection.createStatement(); String q2 = "insert into Orders values ( '" + orderId + "', '" + productId + "', " + amount + " )"; s2.executeUpdate(q2); s2.close(); inventoryConnection.close(); orderConnection.close(); } catch (Exception e) { rollback = true; } finally { if (!rollback) utx.commit(); else utx.rollback(); } }

Here, we are updating the database tables for inventory and order within the transaction boundary. This automatically provides the benefit of these operations happening atomically.

5.4. Testing Transactional Behavior

Finally, we must be able to test our application with simple unit tests to validate that the transaction behavior is as expected:

@Test public void testPlaceOrderSuccess() throws Exception { int amount = 1; long initialBalance = getBalance(inventoryDataSource, productId); Application application = new Application(inventoryDataSource, orderDataSource); application.placeOrder(productId, amount); long finalBalance = getBalance(inventoryDataSource, productId); assertEquals(initialBalance - amount, finalBalance); } @Test public void testPlaceOrderFailure() throws Exception { int amount = 10; long initialBalance = getBalance(inventoryDataSource, productId); Application application = new Application(inventoryDataSource, orderDataSource); application.placeOrder(productId, amount); long finalBalance = getBalance(inventoryDataSource, productId); assertEquals(initialBalance, finalBalance); }

Here, we're expecting a valid order to decrease the inventory, while we're expecting an invalid order to leave the inventory unchanged. Please note that, as per our database constraint, any order with a quantity of more than five of a product is considered an invalid order.

5.5. Advanced Atomikos Usage

The example above is the simplest way to use Atomikos and perhaps sufficient for most of the requirements. However, there are other ways in which we can use Atomikos to build our application. While some of these options make Atomikos easy to use, others offer more flexibility. The choice depends on our requirements.

Of course, it's not necessary to always use Atomikos adapters for JDBC/JMS. We can choose to use the Atomikos transaction manager while working directly with XAResource. However, in that case, we have to explicitly take care of enlisting and delisting XAResource instances with the transaction service.

Atomikos also makes it possible to use more advanced features through a proprietary interface, UserTransactionService. Using this interface, we can explicitly register resources for recovery. This gives us fine-grained control over what resources should be recovered, how they should be recovered, and when recovery should happen.

6. Integrating Atomikos

While Atomikos provides excellent support for distributed transactions, it's not always convenient to work with such low-level APIs. To focus on the business domain and avoid the clutter of boilerplate code, we often need the support of different frameworks and libraries. Atomikos supports most of the popular Java frameworks related to back-end integrations. We'll explore a couple of them here.

6.1. Atomikos With Spring and DataSource

Spring is one of the popular frameworks in Java that provides an Inversion of Control (IoC) container. Notably, it has fantastic support for transactions as well. It offers declarative transaction management using Aspect-Oriented Programming (AOP) techniques.

Spring supports several transaction APIs, including JTA for distributed transactions. We can use Atomikos as our JTA transaction manager within Spring without much effort. Most importantly, our application remains pretty much agnostic to Atomikos, thanks to Spring.

Let's see how we can solve our previous problem, this time leveraging Spring. We'll begin by rewriting the Application class:

public class Application { private DataSource inventoryDataSource; private DataSource orderDataSource; public Application(DataSource inventoryDataSource, DataSource orderDataSource) { this.inventoryDataSource = inventoryDataSource; this.orderDataSource = orderDataSource; } @Transactional(rollbackFor = Exception.class) public void placeOrder(String productId, int amount) throws Exception { String orderId = UUID.randomUUID().toString(); Connection inventoryConnection = inventoryDataSource.getConnection(); Connection orderConnection = orderDataSource.getConnection(); Statement s1 = inventoryConnection.createStatement(); String q1 = "update Inventory set balance = balance - " + amount + " where productId ='" + productId + "'"; s1.executeUpdate(q1); s1.close(); Statement s2 = orderConnection.createStatement(); String q2 = "insert into Orders values ( '" + orderId + "', '" + productId + "', " + amount + " )"; s2.executeUpdate(q2); s2.close(); inventoryConnection.close(); orderConnection.close(); } }

As we can see here, most of the transaction-related boilerplate code has been replaced by a single annotation at the method level. Moreover, Spring takes care of instantiating and injecting DataSource, which our application depends on.

Of course, we have to provide relevant configurations to Spring. We can use a simple Java class to configure these elements:

@Configuration @EnableTransactionManagement public class Config { @Bean(initMethod = "init", destroyMethod = "close") public AtomikosDataSourceBean inventoryDataSource() { AtomikosDataSourceBean dataSource = new AtomikosDataSourceBean(); // Configure database holding order data return dataSource; } @Bean(initMethod = "init", destroyMethod = "close") public AtomikosDataSourceBean orderDataSource() { AtomikosDataSourceBean dataSource = new AtomikosDataSourceBean(); // Configure database holding order data return dataSource; } @Bean(initMethod = "init", destroyMethod = "close") public UserTransactionManager userTransactionManager() throws SystemException { UserTransactionManager userTransactionManager = new UserTransactionManager(); userTransactionManager.setTransactionTimeout(300); userTransactionManager.setForceShutdown(true); return userTransactionManager; } @Bean public JtaTransactionManager jtaTransactionManager() throws SystemException { JtaTransactionManager jtaTransactionManager = new JtaTransactionManager(); jtaTransactionManager.setTransactionManager(userTransactionManager()); jtaTransactionManager.setUserTransaction(userTransactionManager()); return jtaTransactionManager; } @Bean public Application application() { return new Application(inventoryDataSource(), orderDataSource()); } }

Here, we are configuring AtomikosDataSourceBean for the two different databases holding our inventory and order data. Moreover, we're also providing the necessary configuration for the JTA transaction manager.

Now, we can test our application for transactional behavior as before. Again, we should be validating that a valid order reduces our inventory balance, while an invalid order leaves it unchanged.

6.2. Atomikos With Spring, JPA, and Hibernate

While Spring has helped us cut down boilerplate code to a certain extent, it's still quite verbose. Some tools can make working with relational databases in Java even easier. Java Persistence API (JPA) is a specification that describes the management of relational data in Java applications. This simplifies the data access and manipulation code to a large extent.

Hibernate is one of the most popular implementations of the JPA specification. Atomikos has great support for several JPA implementations, including Hibernate. As before, our application remains agnostic to Atomikos as well as Hibernate, thanks to Spring and JPA!

Let's see how Spring, JPA, and Hibernate can make our application even more concise while providing the benefits of distributed transactions through Atomikos. As before, we will begin by rewriting the Application class:

public class Application { @Autowired private InventoryRepository inventoryRepository; @Autowired private OrderRepository orderRepository; @Transactional(rollbackFor = Exception.class) public void placeOrder(String productId, int amount) throws SQLException { String orderId = UUID.randomUUID().toString(); Inventory inventory = inventoryRepository.findOne(productId); inventory.setBalance(inventory.getBalance() - amount); inventoryRepository.save(inventory); Order order = new Order(); order.setOrderId(orderId); order.setProductId(productId); order.setAmount(new Long(amount)); orderRepository.save(order); } }

As we can see, we're not dealing with any low-level database APIs now. However, for this magic to work, we do need to configure Spring Data JPA classes and configurations. We'll begin by defining our domain entities:

@Entity @Table(name = "INVENTORY") public class Inventory { @Id private String productId; private Long balance; // Getters and Setters }
@Entity @Table(name = "ORDERS") public class Order { @Id private String orderId; private String productId; @Max(5) private Long amount; // Getters and Setters }

Next, we need to provide the repositories for these entities:

@Repository public interface InventoryRepository extends JpaRepository { } @Repository public interface OrderRepository extends JpaRepository { }

These are quite simple interfaces, and Spring Data takes care of elaborating these with actual code to work with database entities.

Finally, we need to provide the relevant configurations for a data source for both inventory and order databases and the transaction manager:

@Configuration @EnableJpaRepositories(basePackages = "com.baeldung.atomikos.spring.jpa.inventory", entityManagerFactoryRef = "inventoryEntityManager", transactionManagerRef = "transactionManager") public class InventoryConfig { @Bean(initMethod = "init", destroyMethod = "close") public AtomikosDataSourceBean inventoryDataSource() { AtomikosDataSourceBean dataSource = new AtomikosDataSourceBean(); // Configure the data source return dataSource; } @Bean public EntityManagerFactory inventoryEntityManager() { HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean(); factory.setJpaVendorAdapter(vendorAdapter); // Configure the entity manager factory return factory.getObject(); } }
@Configuration @EnableJpaRepositories(basePackages = "com.baeldung.atomikos.spring.jpa.order", entityManagerFactoryRef = "orderEntityManager", transactionManagerRef = "transactionManager") public class OrderConfig { @Bean(initMethod = "init", destroyMethod = "close") public AtomikosDataSourceBean orderDataSource() { AtomikosDataSourceBean dataSource = new AtomikosDataSourceBean(); // Configure the data source return dataSource; } @Bean public EntityManagerFactory orderEntityManager() { HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean(); factory.setJpaVendorAdapter(vendorAdapter); // Configure the entity manager factory return factory.getObject(); } }
@Configuration @EnableTransactionManagement public class Config { @Bean(initMethod = "init", destroyMethod = "close") public UserTransactionManager userTransactionManager() throws SystemException { UserTransactionManager userTransactionManager = new UserTransactionManager(); userTransactionManager.setTransactionTimeout(300); userTransactionManager.setForceShutdown(true); return userTransactionManager; } @Bean public JtaTransactionManager transactionManager() throws SystemException { JtaTransactionManager jtaTransactionManager = new JtaTransactionManager(); jtaTransactionManager.setTransactionManager(userTransactionManager()); jtaTransactionManager.setUserTransaction(userTransactionManager()); return jtaTransactionManager; } @Bean public Application application() { return new Application(); } }

This is still quite a lot of configuration that we have to do. This is partly because we're configuring Spring JPA for two separate databases. Also, we can further reduce these configurations through Spring Boot, but that's beyond the scope of this tutorial.

As before, we can test our application for the same transactional behavior. There's nothing new this time, except for the fact that we're using Spring Data JPA with Hibernate now.

7. Atomikos Beyond JTA

While JTA provides excellent transaction support for distributed systems, these systems must be XA-complaint like most relational databases or message queues. However, JTA is not useful if one of these systems doesn't support XA specification for a two-phase commit protocol. Several resources fall under this category, especially within a microservices architecture.

Several alternative protocols support distributed transactions. One of these is a variation of two-phase commit protocol that makes use of compensations. Such transactions have a relaxed isolation guarantee and are known as compensation-based transactions. Participants commit the individual parts of the transaction in the first phase itself, offering a compensation handler for a possible rollback in the second phase.

There are several design patterns and algorithms to implement a compensation-based transaction. For example, Sagas is one such popular design pattern. However, they are usually complex to implement and error-prone.

Atomikos offers a variation of compensation-based transaction called Try-Confirm/Cancel (TCC). TCC offers better business semantics to the entities under a transaction. However, this is possible only with advanced architecture support from the participants, and TCC is only available under the Atomikos commercial offering, ExtremeTransactions.

8. Alternatives to Atomikos

We have gone through enough of Atomikos to appreciate what it has to offer. Moreover, there's a commercial offering from Atomikos with even more powerful features. However, Atomikos is not the only option when it comes to choosing a JTA transaction manager. There are a few other credible options to choose from. Let's see how they fare against Atomikos.

8.1. Narayana

Narayana is perhaps one of the oldest open-source distributed transaction managers and is currently managed by Red Hat. It has been widely used across the industry, and it has evolved through community support and influenced several specifications and standards.

Narayana provides support for a wide range of transaction protocols like JTA, JTS, Web-Services, and REST, to name a few. Further, Narayana can be embedded in a wide range of containers.

Compared to Atomikos, Narayana provides pretty much all the features of a distributed transaction manager. In many cases, Narayana is more flexible to integrate and use in applications. For instance, Narayana has language bindings for both C/C++ and Java. However, this comes at the cost of added complexity, and Atomikos is comparatively easier to configure and use.

8.2. Bitronix

Bitronix is a fully working XA transaction manager that provides all services required by the JTA API. Importantly, Bitronix is an embeddable transaction library that provides extensive and useful error reporting and logging. For a distributed transaction, this makes it easier to investigate failures. Moreover, it has excellent support for Spring's transactional capabilities and works with minimal configurations.

Compared to Atomikos, Bitronix is an open-source project and does not have a commercial offering with product support. The key features that are part of Atomikos' commercial offering but are lacking in Bitronix include support for microservices and declarative elastic scaling capability.

9. Conclusion

To sum up, in this tutorial, we went through the basic details of transactions. We understood what distributed transactions are and how a library like Atomikos can facilitate in performing them. In the process, we leveraged the Atomikos APIs to create a simple application with distributed transactions.

We also understood how Atomikos works with other popular Java frameworks and libraries. Finally, we went through some of the alternatives to Atomikos that are available to us.

As usual, the source code for this article can be found over on GitHub.