Anyone who understands affairs knows that most problems can be solved by transaction management alone in our daily development, but why do we need to bring up JTA? What is JTA? What problem is he specifically here to solve?
JTA
JTA (Java Transaction API) is an API used to manage distributed transactions on the Java platform. Itprovides a set of interfaces and classes for coordinating and controlling transaction operations across multiple resources (such as databases, message queues, etc.).
The architecture system of JTA is as follows:
The main goal of JTA is to ensure atomicity, consistency, isolation, and durability (ACID properties) of transactions in a distributed environment. It does this through several key concepts and components:
-
Transaction Manager: Responsible for coordinating and managing the start, commit and rollback of transactions. It is the core component of JTA and is responsible for tracking and controlling the status of transactions.
-
User Transaction: Represents a transaction initiated by the application, managed and controlled through the transaction manager.
-
XA Resource Manager: Represents resources in a distributed environment, such as databases, message queues, etc. It implements the XA interface and can participate in distributed transactions.
-
XA Transaction: Represents a distributed transaction across multiple XA resource managers. It follows the XA protocol and ensures transaction consistency through Two-Phase Commit.
Using JTA, developers can write applications with transaction guarantees in a distributed environment. It provides a standardized way to handle distributed transactions, simplifying developers’ work while ensuring data consistency and reliability.
JTA transactions are more powerful than our commonly used JDBC transactions. A JTA transaction can have multiple participants, while a JDBC transaction is not limited to a single database connection.
Let me put it this way, let me give you an example:
When we use multiple data sources, assume that our updates to data source A and data source B are transactional. For example: we create a new order data in the order, and I also need to update related products in the product library. Deduction of inventory. Assuming that we fail to deduct inventory, then we definitely hope that our order will return to the state before the order was placed. After all, I placed the order and the inventory has not been reduced. What kind of place do I think this is? Placed the order.
If these two pieces of data are located in a database, then we can complete the operation through simple transaction management, and we can end it here. But if our two operations are in different databases, what should we do? Woolen cloth?
So let’s test it:
Introducing relevant dependencies into Spring Boot:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!--Focus on this dependency--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jta-atomikos</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
Then configure the Spring Boot application to connect to the database:
spring.jta.enabled=true spring.jta.atomikos.datasource.primary.xa-properties.url=jdbc:mysql://localhost:3306/test1?useUnicode=true &characterEncoding=utf-8 &useSSL=true &serverTimezone= UTC spring.jta.atomikos.datasource.primary.xa-properties.user=root spring.jta.atomikos.datasource.primary.xa-properties.password=123456 spring.jta.atomikos.datasource.primary.xa-data-source-class-name=com.mysql.cj.jdbc.MysqlXADataSource spring.jta.atomikos.datasource.primary.unique-resource-name=test1 spring.jta.atomikos.datasource.primary.max-pool-size=25 spring.jta.atomikos.datasource.primary.min-pool-size=3 spring.jta.atomikos.datasource.primary.max-lifetime=20000 spring.jta.atomikos.datasource.primary.borrow-connection-timeout=10000 spring.jta.atomikos.datasource.secondary.xa-properties.url=jdbc:mysql://localhost:3306/test2?useUnicode=true &characterEncoding=utf-8 &useSSL=true &serverTimezone= UTC spring.jta.atomikos.datasource.secondary.xa-properties.user=root spring.jta.atomikos.datasource.secondary.xa-properties.password=123456 spring.jta.atomikos.datasource.secondary.xa-data-source-class-name=com.mysql.cj.jdbc.MysqlXADataSource spring.jta.atomikos.datasource.secondary.unique-resource-name=test2 spring.jta.atomikos.datasource.secondary.max-pool-size=25 spring.jta.atomikos.datasource.secondary.min-pool-size=3 spring.jta.atomikos.datasource.secondary.max-lifetime=20000 spring.jta.atomikos.datasource.secondary.borrow-connection-timeout=10000
@Configuration public class DataSourceConfiguration {<!-- --> @Primary @Bean @ConfigurationProperties(prefix = "spring.jta.atomikos.datasource.primary") public DataSource primaryDataSource() {<!-- --> return new AtomikosDataSourceBean(); } @Bean @ConfigurationProperties(prefix = "spring.jta.atomikos.datasource.secondary") public DataSource secondaryDataSource() {<!-- --> return new AtomikosDataSourceBean(); } @Bean public JdbcTemplate primaryJdbcTemplate(@Qualifier("primaryDataSource") DataSource primaryDataSource) {<!-- --> return new JdbcTemplate(primaryDataSource); } @Bean public JdbcTemplate secondaryJdbcTemplate(@Qualifier("secondaryDataSource") DataSource secondaryDataSource) {<!-- --> return new JdbcTemplate(secondaryDataSource); } }
Create a test Service to verify whether our JTA can complete the work we want.
@Service public class TestService {<!-- --> private JdbcTemplate primaryJdbcTemplate; private JdbcTemplate secondaryJdbcTemplate; public TestService(JdbcTemplate primaryJdbcTemplate, JdbcTemplate secondaryJdbcTemplate) {<!-- --> this.primaryJdbcTemplate = primaryJdbcTemplate; this.secondaryJdbcTemplate = secondaryJdbcTemplate; } @Transactional public void tx() {<!-- --> //Modify the data in the test1 library primaryJdbcTemplate.update("update user set age = ? where name = ?", 30, "aaa"); //Modify the data in the test2 library secondaryJdbcTemplate.update("update user set age = ? where name = ?", 30, "aaa"); } @Transactional public void tx2() {<!-- --> //Modify the data in the test1 library primaryJdbcTemplate.update("update user set age = ? where name = ?", 40, "aaa"); // Simulation: throw an exception before modifying the test2 library throw new RuntimeException(); } }
In the above operation, we define the tx method, which will generally succeed, but in the tx2 method, we define an exception for it ourselves. This will be generated after the test1 database is updated, so that we can test that after the test1 update is successful, , whether it is possible to achieve rollback with the help of JTA.
Create a unit test class:
@SpringBootTest(classes = Application.class) public class ApplicationTests {<!-- --> @Autowired protected JdbcTemplate primaryJdbcTemplate; @Autowired protected JdbcTemplate secondaryJdbcTemplate; @Autowired private TestService testService; @Test public void test1() throws Exception {<!-- --> // Correct update situation testService.tx(); Assertions.assertEquals(30, primaryJdbcTemplate.queryForObject("select age from user where name=?", Integer.class, "aaa")); Assertions.assertEquals(30, secondaryJdbcTemplate.queryForObject("select age from user where name=?", Integer.class, "aaa")); } @Test public void test2() throws Exception {<!-- --> // Update failure situation try {<!-- --> testService.tx2(); } catch (Exception e) {<!-- --> e.printStackTrace(); } finally {<!-- --> // Some updates failed, updates in test1 should be rolled back Assertions.assertEquals(30, primaryJdbcTemplate.queryForObject("select age from user where name=?", Integer.class, "aaa")); Assertions.assertEquals(30, secondaryJdbcTemplate.queryForObject("select age from user where name=?", Integer.class, "aaa")); } } }
For the above test case:
test1: Because there are no intentionally created exceptions, usually the updates of both libraries will be successful. Then we check out the two data based on name=aaa to see if the age has been updated to 30.
test2: The tx2 function will update the age of the user with name=aaa in test1 to 40, and then throw an exception. If the JTA transaction takes effect, the age will be rolled back to 30, so the check here is also the age of the aaa user of the two libraries. Both are 30, which means that the JTA transaction takes effect, ensuring that the User table data in the two libraries test1 and test2 are updated consistently, and no dirty data is produced.