1. Въведение
Atomikos е библиотека за транзакции за Java приложения . В този урок ще разберем защо и как да използваме Atomikos.
В процеса ще разгледаме и основите на транзакциите и защо имаме нужда от тях.
След това ще създадем просто приложение с транзакции, използващи различни API от Atomikos.
2. Разбиране на основите
Преди да обсъдим Atomikos, нека разберем какво точно представляват транзакциите и няколко понятия, свързани с тях. Казано по-просто, транзакцията е логическа единица работа, чийто ефект е видим извън транзакцията или изцяло, или изобщо не .
Да вземем пример, за да разберем това по-добре. Типично приложение за продажба на дребно запазва инвентара и след това прави поръчка:

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

Тук инвентаризацията и поръчките могат да бъдат две таблици в една и съща база данни или те могат да бъдат две различни бази данни - евентуално работещи на различни машини.
2.2. XA спецификация и API за транзакции на Java
XA се отнася до eXtended Architecture, която е спецификация за обработка на разпределени транзакции. На вратата на ХА е да се осигури атомност в глобалните сделки с разнородни компоненти .
Спецификацията XA осигурява целостта чрез протокол, известен като двуфазен ангажимент. Двуфазното фиксиране е широко използван разпределен алгоритъм за улесняване на решението за ангажиране или връщане на разпределена транзакция.
Java Transaction API (JTA) е API на Java Enterprise Edition, разработен в рамките на процеса на общността на Java. Той позволява на Java приложенията и сървърите на приложения да извършват разпределени транзакции между XA ресурси .
JTA е моделиран около XA архитектура, използвайки двуфазен ангажимент. JTA определя стандартни Java интерфейси между мениджъра на транзакции и другите страни в разпределена транзакция.
3. Въведение в Atomikos
След като преминахме през основите на транзакциите, сме готови да научим Atomikos. В този раздел ще разберем какво точно представлява Atomikos и как той е свързан с понятия като XA и JTA. Също така ще разберем архитектурата на Atomikos и ще разгледаме нейните продуктови предложения.
3.1. Какво е Атомикос
Както видяхме, JTA предоставя интерфейси в Java за изграждане на приложения с разпределени транзакции. Сега JTA е само спецификация и не предлага никакво изпълнение. За да стартираме приложение, където използваме JTA, имаме нужда от внедряване на JTA . Такова изпълнение се нарича мениджър на транзакции.
Обикновено сървърът на приложения осигурява изпълнение по подразбиране на мениджъра на транзакции. Например, в случай на Enterprise Java Beans (EJB), контейнерите EJB управляват поведението на транзакциите без изрична намеса от разработчиците на приложения. В много случаи обаче това може да не е идеално и може да се наложи директен контрол върху транзакцията, независим от сървъра на приложения.
Atomikos е лек мениджър на транзакции за Java, който позволява приложенията, използващи разпределени транзакции, да бъдат самостоятелни. По същество нашето приложение не трябва да разчита на тежък компонент като сървър на приложения за транзакции. Това доближава концепцията за разпределени транзакции до архитектура, която е родна в облака.
3.2. Атомикос Архитектура
Atomikos е изграден предимно като JTA транзакционен мениджър и следователно реализира XA архитектура с двуфазен протокол за фиксиране . Нека видим архитектура на високо ниво с Atomikos:

Тук Atomikos улеснява двуфазна транзакция, базирана на ангажиране, обхващаща база данни и опашка за съобщения.
3.3. Предложения за продукти на Atomikos
Atomikos е разпределен мениджър на транзакции, който предлага повече функции от това, което JTA / XA урежда. Той има продукт с отворен код и много по-изчерпателно търговско предложение:
- TransactionEssentials: Продуктът на Atomikos с отворен код, предоставящ JTA / XA мениджър на транзакции за Java приложения, работещи с бази данни и опашки за съобщения. Това е най-полезно за целите на тестването и оценката.
- ExtremeTransactions: търговското предложение на Atomikos, което предлага разпределени транзакции в композитни приложения, включително REST услуги, освен бази данни и опашки за съобщения. Това е полезно за изграждане на приложения, извършващи екстремна обработка на транзакции (XTP).
В този урок ще използваме библиотеката TransactionsEssentials, за да изградим и демонстрираме възможностите на Atomikos.
4. Настройване на Atomikos
Както видяхме по-рано, един от акцентите на Atomikos е, че това е вградена услуга за транзакции . Това означава, че можем да го стартираме в същия JVM като нашето приложение. По този начин настройването на Atomikos е съвсем просто.
4.1. Зависимости
Първо, трябва да настроим зависимостите. Тук всичко, което трябва да направим, е да декларираме зависимостите в нашия файл Maven pom.xml :
com.atomikos transactions-jdbc 5.0.6 com.atomikos transactions-jms 5.0.6
В този случай използваме зависимости Atomikos за JDBC и JMS, но подобни зависимости са налични в Maven Central за други ресурси за жалби XA.
4.2. Конфигурации
Atomikos предоставя няколко конфигурационни параметъра, със разумни настройки по подразбиране за всеки от тях. Най-лесният начин да се преодолеят тези параметри е да се осигури transactions.properties файл в CLASSPATH . Можем да добавим няколко параметъра за инициализация и работа на услугата за транзакции. Нека видим проста конфигурация, която да замени директорията, в която се създават регистрационни файлове:
com.atomikos.icatch.file=path_to_your_file
По същия начин има и други параметри, които можем да използваме, за да контролираме времето за изчакване за транзакции, да зададем уникални имена за нашето приложение или да дефинираме поведение при изключване.
4.3. Бази данни
В нашия урок ще създадем просто приложение за търговия на дребно, като описаното по-рано, което запазва инвентара и след това прави поръчка. За простота ще използваме релационна база данни. Освен това ще използваме множество бази данни, за да демонстрираме разпределени транзакции. Това обаче може много добре да се разпростре и върху други ресурси за жалби XA, като опашки за съобщения и теми .
Нашата база данни за инвентара ще има проста таблица за приемане на инвентаризации на продукти:
CREATE TABLE INVENTORY ( productId VARCHAR PRIMARY KEY, balance INT );
И нашата база данни за поръчки ще има проста таблица за хостване на направени поръчки:
CREATE TABLE ORDERS ( orderId VARCHAR PRIMARY KEY, productId VARCHAR, amount INT NOT NULL CHECK (amount <= 5) );
Това е много основна схема на базата данни и полезна само за демонстрация. Важно е обаче да се отбележи, че ограничението на нашата схема не позволява поръчка с количество на продукта повече от пет.
5. Работа с Atomikos
Сега сме готови да използваме една от библиотеките на Atomikos, за да изградим нашето приложение с разпределени транзакции. В следващите подраздели ще използваме вградените адаптери за ресурси на Atomikos, за да се свързваме с нашите системи за база данни. Това е най-бързият и лесен начин да започнете с Atomikos .
5.1. създаване на екземпляр UserTransaction
Ще използваме JTA UserTransaction, за да очертаем границите на транзакциите. Всички останали стъпки, свързани с услугата за транзакции, ще бъдат автоматично поети . Това включва записване и делистиране на ресурси с услугата за транзакции.
Първо, трябва да създадем екземпляр на UserTransaction от Atomikos:
UserTransactionImp utx = new UserTransactionImp();
5.2. Инсталиране на източника на данни
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.