Una guida ad Atomikos

1. Introduzione

Atomikos è una libreria di transazioni per applicazioni Java . In questo tutorial capiremo perché e come usare Atomikos.

Nel processo, esamineremo anche le basi delle transazioni e il motivo per cui ne abbiamo bisogno.

Quindi, creeremo una semplice applicazione con transazioni che sfruttano diverse API di Atomikos.

2. Comprensione delle basi

Prima di discutere di Atomikos, capiamo cosa sono esattamente le transazioni e alcuni concetti ad esse correlati. In parole povere, una transazione è un'unità logica di lavoro il cui effetto è visibile al di fuori della transazione nella sua interezza o per niente .

Facciamo un esempio per capirlo meglio. Una tipica applicazione di vendita al dettaglio riserva l'inventario e quindi effettua un ordine:

Qui, vorremmo che queste due operazioni avvengano insieme o non avvengano affatto. Possiamo ottenere ciò raggruppando queste operazioni in una singola transazione.

2.1. Transazione locale e distribuita

Una transazione può comportare più operazioni indipendenti. Queste operazioni possono essere eseguite sulla stessa risorsa o su risorse diverse . Ci riferiamo ai componenti che partecipano a una transazione come un database come risorsa qui.

Le transazioni all'interno di una singola risorsa sono note transazioni locali mentre quelle che si generano su più risorse sono note come transazioni distribuite:

In questo caso, inventario e ordini possono essere due tabelle nello stesso database oppure possono essere due database diversi, possibilmente in esecuzione su macchine diverse.

2.2. Specifica XA e API di transazione Java

XA si riferisce a eXtended Architecture, che è una specifica per l'elaborazione delle transazioni distribuite. L' obiettivo di XA è fornire atomicità nelle transazioni globali che coinvolgono componenti eterogenee .

La specifica XA fornisce integrità tramite un protocollo noto come commit a due fasi. Il commit in due fasi è un algoritmo distribuito ampiamente utilizzato per facilitare la decisione di eseguire il commit o il rollback di una transazione distribuita.

Java Transaction API (JTA) è un'API Java Enterprise Edition sviluppata nell'ambito del Java Community Process. Consente alle applicazioni Java e ai server delle applicazioni di eseguire transazioni distribuite tra le risorse XA .

JTA è modellato attorno all'architettura XA, sfruttando il commit in due fasi. JTA specifica le interfacce Java standard tra un gestore delle transazioni e le altre parti in una transazione distribuita.

3. Introduzione ad Atomikos

Ora che abbiamo esaminato le basi delle transazioni, siamo pronti per imparare Atomikos. In questa sezione, capiremo cos'è esattamente Atomikos e come si relaziona a concetti come XA e JTA. Comprenderemo anche l'architettura di Atomikos e analizzeremo la sua offerta di prodotti.

3.1. Cos'è Atomikos

Come abbiamo visto, JTA fornisce interfacce in Java per la creazione di applicazioni con transazioni distribuite. Ora, JTA è solo una specifica e non offre alcuna implementazione. Per poter eseguire un'applicazione in cui sfruttiamo JTA, abbiamo bisogno di un'implementazione di JTA . Tale implementazione è chiamata gestore delle transazioni.

In genere, il server delle applicazioni fornisce un'implementazione predefinita del gestore delle transazioni. Ad esempio, nel caso di Enterprise Java Beans (EJB), i contenitori EJB gestiscono il comportamento delle transazioni senza alcun intervento esplicito da parte degli sviluppatori dell'applicazione. Tuttavia, in molti casi, questo potrebbe non essere l'ideale e potrebbe essere necessario un controllo diretto sulla transazione indipendentemente dal server delle applicazioni.

Atomikos è un gestore di transazioni leggero per Java che consente alle applicazioni che utilizzano transazioni distribuite di essere autonome. In sostanza, la nostra applicazione non ha bisogno di fare affidamento su un componente pesante come un application server per le transazioni. Ciò avvicina il concetto di transazioni distribuite a un'architettura nativa del cloud.

3.2. Atomikos Architecture

Atomikos è costruito principalmente come gestore di transazioni JTA e, quindi, implementa l'architettura XA con un protocollo di commit a due fasi . Vediamo un'architettura di alto livello con Atomikos:

Atomikos sta facilitando una transazione basata su commit in due fasi che si estende su un database e una coda di messaggi.

3.3. Offerte di prodotti Atomikos

Atomikos è un gestore di transazioni distribuito che offre più funzionalità rispetto a quanto richiesto da JTA / XA. Ha un prodotto open source e un'offerta commerciale molto più completa:

  • TransactionsEssentials: il prodotto open source di Atomikos che fornisce un gestore delle transazioni JTA / XA per applicazioni Java che lavorano con database e code di messaggi. Ciò è principalmente utile per scopi di test e valutazione.
  • ExtremeTransactions: l' offerta commerciale di Atomikos, che offre transazioni distribuite su applicazioni composite, inclusi servizi REST oltre a database e code di messaggi. Ciò è utile per creare applicazioni che eseguono Extreme Transaction Processing (XTP).

In questo tutorial, utilizzeremo la libreria TransactionsEssentials per creare e dimostrare le capacità di Atomikos.

4. Configurazione di Atomikos

Come abbiamo visto in precedenza, uno dei punti salienti di Atomikos è che si tratta di un servizio di transazione integrato . Ciò significa che possiamo eseguirlo nella stessa JVM della nostra applicazione. Pertanto, configurare Atomikos è abbastanza semplice.

4.1. Dipendenze

Innanzitutto, dobbiamo impostare le dipendenze. Qui, tutto ciò che dobbiamo fare è dichiarare le dipendenze nel nostro file Maven pom.xml :

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

In questo caso stiamo utilizzando le dipendenze Atomikos per JDBC e JMS, ma dipendenze simili sono disponibili su Maven Central per altre risorse di reclamo XA.

4.2. Configurazioni

Atomikos fornisce diversi parametri di configurazione, con valori predefiniti ragionevoli per ciascuno di essi. Il modo più semplice per sovrascrivere questi parametri è fornire un file transaction.properties nel classpath . Possiamo aggiungere diversi parametri per l'inizializzazione e il funzionamento del servizio di transazione. Vediamo una semplice configurazione per sovrascrivere la directory in cui vengono creati i file di log:

com.atomikos.icatch.file=path_to_your_file

Allo stesso modo, ci sono altri parametri che possiamo utilizzare per controllare il timeout per le transazioni, impostare nomi univoci per la nostra applicazione o definire il comportamento di spegnimento.

4.3. Banche dati

Nel nostro tutorial creeremo una semplice applicazione di vendita al dettaglio, come quella descritta in precedenza, che prenota l'inventario e quindi effettua un ordine. Useremo un database relazionale per semplicità. Inoltre, utilizzeremo più database per dimostrare le transazioni distribuite. Tuttavia, questo può benissimo estendersi ad altre risorse di reclamo XA come code di messaggi e argomenti .

Il nostro database di inventario avrà una semplice tabella per ospitare gli inventari dei prodotti:

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

Inoltre, il nostro database degli ordini avrà una semplice tabella per ospitare gli ordini effettuati:

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

Questo è uno schema di database molto semplice e utile solo per la dimostrazione. Tuttavia, è importante notare che il nostro vincolo di schema non consente ordini con una quantità di prodotto superiore a cinque.

5. Lavorare con 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.