在Spring Boot中使用JTA实现对多数据源的事务管理

了解事务的都知道,在我们日常开发中单单靠事务管理就可以解决绝大多数问题了,但是为啥还要提出JTA这个玩意呢,到底JTA是什么呢?他又是具体来解决啥问题的呢?

JTA

JTA(Java Transaction API)是Java平台上用于管理分布式事务的API 。它提供了一组接口和类,用于协调和控制跨多个资源(如数据库、消息队列等)的事务操作

JTA的架构体系如下:

JTA的主要目标是确保分布式环境中的事务的原子性、一致性、隔离性和持久性(ACID属性)。它通过以下几个关键概念和组件来实现:

  • 事务管理器(Transaction Manager) :负责协调和管理事务的开始、提交和回滚等操作。它是JTA的核心组件,负责跟踪和控制事务的状态。
  • 用户事务(User Transaction) :表示应用程序发起的事务,通过事务管理器来管理和控制。
  • XA资源管理器(XA Resource Manager) :表示分布式环境中的资源,如数据库、消息队列等。它实现了XA接口,可以参与到分布式事务中。
  • XA事务(XA Transaction) :表示跨多个XA资源管理器的分布式事务。它遵循XA协议,通过两阶段提交(Two-Phase Commit)来保证事务的一致性。

使用JTA,开发人员可以在分布式环境中编写具有事务保证的应用程序。它提供了一种标准化的方式来处理分布式事务,简化了开发人员的工作,同时确保了数据的一致性和可靠性。

JTA事务比我们常用的JDBC事务更加强大,一个JTA事务可以有多个参与者,而一个JDBC事务则别限定在一个单一的数据库连接。

这么说吧,我举个栗子:

我们采用多数据源的时候,假设我们对A数据源的更新与B数据源的更新具有事务性,比如:我们对订单中创建一条新的订单数据,同时我也需要在商品库中进行相关商品的扣减库存,假设我们对库存进行扣减失败了,那么我们肯定希望我们的订单也返回到之前没下订单之前的状态,毕竟我下了订单了,库存没减少,我这算哪门子的下了订单。

如果这两条数据位于一个数据库,那么我们可以通过简单的事务管理就可以完成操作,那么我们至此就可以结束了,但是当我们的这两个操作要是在不同的数据库中,那么我们该怎么办呢?

那么我们就来测试一下:

Spring Boot中引入相关依赖:

xml 复制代码
		<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>

		<!--重点围绕这个依赖-->
		<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>

之后再Spring Boot application配置连接数据库的相关配置:

properties 复制代码
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
java 复制代码
@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);
    }

}

创建一个测试Service用来校验我们的JTA是否可以完成我们想要的工作。

java 复制代码
@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() {
        // 修改test1库中的数据
        primaryJdbcTemplate.update("update user set age = ? where name = ?", 30, "aaa");
        // 修改test2库中的数据
        secondaryJdbcTemplate.update("update user set age = ? where name = ?", 30, "aaa");
    }

    @Transactional
    public void tx2() {
        // 修改test1库中的数据
        primaryJdbcTemplate.update("update user set age = ? where name = ?", 40, "aaa");
        // 模拟:修改test2库之前抛出异常
        throw new RuntimeException();
    }
}

在以上操作中,我们定义tx方法中,一般会成功,但tx2方法中,我们自己给他定义了一个异常,这个是在test1数据库更新后才会产生的,这样就可以测试一test1更新成功后,是否还能再JTA的帮助下实现回滚。

创建一个单元测试类:

java 复制代码
@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 {
        // 正确更新的情况
        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 {
        // 更新失败的情况
        try {
            testService.tx2();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 部分更新失败,test1中的更新应该回滚
            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"));
        }
    }
}

对以上测试用例:

test1:因为没有故意制造的异常,一般情况下两个库的update都会成功,然后我们根据name=aaa去把两个数据查出来,看age是否都被更新到了30。

test2:tx2函数会把test1中name=aaa的用户age更新为40,然后抛出异常,JTA事务生效的话,会把age回滚回30,所以这里的检查也是两个库的aaa用户的age应该都为30,这样就意味着JTA事务生效,保证了test1和test2两个库中的User表数据更新一致,没有制造出脏数据。

相关推荐
吃饭最爱5 分钟前
spring高级知识概览
spring boot
舒克日记25 分钟前
基于springboot针对老年人的景区订票系统
java·spring boot·后端
沐雨橙风ιε1 小时前
Spring Boot整合Apache Shiro权限认证框架(实战篇)
java·spring boot·后端·apache shiro
一线大码1 小时前
开发 Java 项目时的命名规范
java·spring boot·后端
本就一无所有 何惧重新开始2 小时前
Redis技术应用
java·数据库·spring boot·redis·后端·缓存
低音钢琴2 小时前
【SpringBoot从初学者到专家的成长11】Spring Boot中的application.properties与application.yml详解
java·spring boot·后端
不太会写3 小时前
又开始了 小程序定制
vue.js·spring boot·python·小程序
MrSYJ4 小时前
学完涨工资的技巧2:Spring Authorization Server如何签发JWTToken
java·spring boot·微服务
摇滚侠5 小时前
Spring Boot 3零基础教程,yml语法细节,笔记16
java·spring boot·笔记