乐趣区

关于java:Spring-Boot-2x基础教程使用JTA实现多数据源的事务管理

在一个 Spring Boot 我的项目中,连贯多个数据源还是比拟常见的。之前也介绍了如何在几种罕用框架的场景下配置多数据源,具体可见:

  • Spring Boot 2.x 基础教程:JdbcTemplate 的多数据源配置
  • Spring Boot 2.x 基础教程:Spring Data JPA 的多数据源配置
  • Spring Boot 2.x 基础教程:MyBatis 的多数据源配置

当咱们采纳多数据源的时候,同时也会呈现一个这样的非凡场景:咱们心愿对 A 数据源的更新和 B 数据源的更新具备事务性。这样的例子很常见,比方:在订单库中创立一条订单记录,同时还须要在商品库中扣减商品库存。如果库存扣减失败,那么咱们心愿订单创立也可能回滚。

如果这两条数据在一个数据库中,那么通过之前介绍的事务管理就能轻松解决了。然而,当这两个操作位于不同的数据库中,那么就无奈实现了。

本文就来介绍一种解决这类问题的办法:JTA 事务。

什么是 JTA

JTA,全称:Java Transaction API。JTA 事务比 JDBC 事务更弱小。一个 JTA 事务能够有多个参与者,而一个 JDBC 事务则被限定在一个繁多的数据库连贯。所以,当咱们在同时操作多个数据库的时候,应用 JTA 事务就能够补救 JDBC 事务的有余。

在 Spring Boot 2.x 中,整合了这两个 JTA 的实现:

  • Atomikos:能够通过引入 spring-boot-starter-jta-atomikos 依赖来应用
  • Bitronix:能够通过引入 spring-boot-starter-jta-bitronix 依赖来应用

因为 Bitronix 自 Spring Boot 2.3.0 开始不举荐应用,所以在上面的入手环节中,咱们将应用 Atomikos 作为例子来介绍 JTA 的应用。

入手试试

上面咱们就来实操一下,如何在 Spring Boot 中应用 JTA 来实现多数据源下的事务管理。

筹备工作

  1. 这里咱们将应用最根底的 JdbcTemplate 来实现数据拜访,所以如果你还不会应用 JdbcTemplate 配置多数据源,倡议先看一下 JdbcTemplate 的多数据源配置。
  2. 场景设定:

    • 假如咱们有两个库,别离为:test1 和 test2
    • 这两个库中都有一张 User 表,咱们心愿这两张表中的数据是统一的
    • 假如这两张表中都曾经有一条数据:name=aaa,age=30;因为这两张表中数据是统一的,所以要 update 的时候,就必须两个库中的 User 表更新时候,要么都胜利,要么都失败。

本文首发:https://blog.didispace.com/spring-boot-learning-24-3-12/,前期批改更新次要以原文为主。

操作具体

  1. pom.xml 中退出 JTA 的实现 Atomikos 的 Starter
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
  1. application.properties 配置文件中配置两个 test1 和 test2 数据源
spring.jta.enabled=true

spring.jta.atomikos.datasource.primary.xa-properties.url=jdbc:mysql://localhost:3306/test1
spring.jta.atomikos.datasource.primary.xa-properties.user=root
spring.jta.atomikos.datasource.primary.xa-properties.password=12345678
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
spring.jta.atomikos.datasource.secondary.xa-properties.user=root
spring.jta.atomikos.datasource.secondary.xa-properties.password=12345678
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
  1. 创立多数据源配置类
@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);
    }

}

留神,这里除了家在的配置不同之外,DataSource也采纳了 AtomikosDataSourceBean 留神与之前配置多数据源应用的配置和实现类的区别。

  1. 创立一个 Service 实现,模仿两种不同的状况。
@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 函数,是两句 update 操作,个别都会胜利;而 tx2 函数中,咱们人为的制作了一个异样,这个异样是在 test1 库中的数据更新后才产生的,这样就能够测试一下 test1 更新胜利,之后是否还能在 JTA 的帮忙下实现回滚。

  1. 创立测试类,编写测试用例
@SpringBootTest(classes = Chapter312Application.class)
public class Chapter312ApplicationTests {

    @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 表数据更新统一,没有制作出脏数据。

测试验证

将下面编写的单元测试运行起来:

察看一下启动阶段的日志,能够看到这些 Atomikos 初始化日志输入:

2021-02-02 19:00:36.145  INFO 8868 --- [main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.default_max_wait_time_on_shutdown = 9223372036854775807
2021-02-02 19:00:36.145  INFO 8868 --- [main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.allow_subtransactions = true
2021-02-02 19:00:36.145  INFO 8868 --- [main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.recovery_delay = 10000
2021-02-02 19:00:36.145  INFO 8868 --- [main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.automatic_resource_registration = true
2021-02-02 19:00:36.145  INFO 8868 --- [main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.oltp_max_retries = 5
2021-02-02 19:00:36.145  INFO 8868 --- [main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.client_demarcation = false
2021-02-02 19:00:36.145  INFO 8868 --- [main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.threaded_2pc = false
2021-02-02 19:00:36.145  INFO 8868 --- [main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.serial_jta_transactions = true
2021-02-02 19:00:36.145  INFO 8868 --- [main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.log_base_dir = /Users/didi/Documents/GitHub/SpringBoot-Learning/2.x/chapter3-12/transaction-logs
2021-02-02 19:00:36.145  INFO 8868 --- [main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.rmi_export_class = none
2021-02-02 19:00:36.145  INFO 8868 --- [main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.max_actives = 50
2021-02-02 19:00:36.145  INFO 8868 --- [main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.checkpoint_interval = 500
2021-02-02 19:00:36.145  INFO 8868 --- [main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.enable_logging = true
2021-02-02 19:00:36.145  INFO 8868 --- [main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.log_base_name = tmlog
2021-02-02 19:00:36.146  INFO 8868 --- [main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.max_timeout = 300000
2021-02-02 19:00:36.146  INFO 8868 --- [main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.trust_client_tm = false
2021-02-02 19:00:36.146  INFO 8868 --- [main] c.a.icatch.provider.imp.AssemblerImp     : USING: java.naming.factory.initial = com.sun.jndi.rmi.registry.RegistryContextFactory
2021-02-02 19:00:36.146  INFO 8868 --- [main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.tm_unique_name = 127.0.0.1.tm
2021-02-02 19:00:36.146  INFO 8868 --- [main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.forget_orphaned_log_entries_delay = 86400000
2021-02-02 19:00:36.146  INFO 8868 --- [main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.oltp_retry_interval = 10000
2021-02-02 19:00:36.146  INFO 8868 --- [main] c.a.icatch.provider.imp.AssemblerImp     : USING: java.naming.provider.url = rmi://localhost:1099
2021-02-02 19:00:36.146  INFO 8868 --- [main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.force_shutdown_on_vm_exit = false
2021-02-02 19:00:36.146  INFO 8868 --- [main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.default_jta_timeout = 10000
2021-02-02 19:00:36.147  INFO 8868 --- [main] c.a.icatch.provider.imp.AssemblerImp     : Using default (local) logging and recovery...
2021-02-02 19:00:36.184  INFO 8868 --- [main] c.a.d.xa.XATransactionalResource         : test1: refreshed XAResource
2021-02-02 19:00:36.203  INFO 8868 --- [main] c.a.d.xa.XATransactionalResource         : test2: refreshed XAResource

同时,咱们在 transaction-logs 目录下,还能找到对于事务的日志信息:

{"id":"127.0.0.1.tm161226409083100001","wasCommitted":true,"participants":[{"uri":"127.0.0.1.tm1","state":"COMMITTING","expires":1612264100801,"resourceName":"test1"},{"uri":"127.0.0.1.tm2","state":"COMMITTING","expires":1612264100801,"resourceName":"test2"}]}
{"id":"127.0.0.1.tm161226409083100001","wasCommitted":true,"participants":[{"uri":"127.0.0.1.tm1","state":"TERMINATED","expires":1612264100804,"resourceName":"test1"},{"uri":"127.0.0.1.tm2","state":"TERMINATED","expires":1612264100804,"resourceName":"test2"}]}
{"id":"127.0.0.1.tm161226409092800002","wasCommitted":false,"participants":[{"uri":"127.0.0.1.tm3","state":"TERMINATED","expires":1612264100832,"resourceName":"test1"}]}

更多本系列收费教程连载「点击进入汇总目录」

代码示例

本文的相干例子能够查看上面仓库中的 chapter3-12 目录:

  • Github:https://github.com/dyc87112/SpringBoot-Learning/
  • Gitee:https://gitee.com/didispace/SpringBoot-Learning/

如果您感觉本文不错,欢送 Star 反对,您的关注是我保持的能源!

欢送关注我的公众号:程序猿 DD,取得独家整顿的收费学习资源助力你的 Java 学习之路!另每周赠书不停哦~

退出移动版