Using JTA to implement transaction management of multiple data sources in Spring Boot

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.