Spring Boot 2.x基础教程:使用JTA实现分布式事务

Spring Boot 2.x基础教程:使用JTA实现分布式事务

作者:翟永超 2021-02-03 10:49:34

开发

开发工具

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

 [[380215]]

在一个Spring Boot项目中,连接多个数据源还是比较常见的。

当我们采用多数据源的时候,同时也会出现一个这样的特殊场景:我们希望对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来实现多数据源下的事务管理。

准备工作

  • 这里我们将使用最基础的JdbcTemplate来实现数据访问,所以如果你还不会使用JdbcTemplate配置多数据源,建议先看一下JdbcTemplate的多数据源配置。

场景设定:

  • 假设我们有两个库,分别为:test1和test2
  • 这两个库中都有一张User表,我们希望这两张表中的数据是一致的
  • 假设这两张表中都已经有一条数据:name=aaa,age=30;因为这两张表中数据是一致的,所以要update的时候,就必须两个库中的User表更新时候,要么都成功,要么都失败。

操作详细

在pom.xml中加入JTA的实现Atomikos的Starter

  1. <dependency> 
  2.     <groupId>org.springframework.boot</groupId> 
  3.     <artifactId>spring-boot-starter-jta-atomikos</artifactId> 
  4. </dependency> 

在application.properties配置文件中配置两个test1和test2数据源

  1. spring.jta.enabled=true 
  2.  
  3. spring.jta.atomikos.datasource.primary.xa-properties.url=jdbc:mysql://localhost:3306/test1 
  4. spring.jta.atomikos.datasource.primary.xa-properties.user=root 
  5. spring.jta.atomikos.datasource.primary.xa-properties.password=12345678 
  6. spring.jta.atomikos.datasource.primary.xa-data-source-class-name=com.mysql.cj.jdbc.MysqlXADataSource 
  7. spring.jta.atomikos.datasource.primary.unique-resource-name=test1 
  8. spring.jta.atomikos.datasource.primary.max-pool-size=25 
  9. spring.jta.atomikos.datasource.primary.min-pool-size=3 
  10. spring.jta.atomikos.datasource.primary.max-lifetime=20000 
  11. spring.jta.atomikos.datasource.primary.borrow-connection-timeout=10000 
  12.  
  13. spring.jta.atomikos.datasource.secondary.xa-properties.url=jdbc:mysql://localhost:3306/test2 
  14. spring.jta.atomikos.datasource.secondary.xa-properties.user=root 
  15. spring.jta.atomikos.datasource.secondary.xa-properties.password=12345678 
  16. spring.jta.atomikos.datasource.secondary.xa-data-source-class-name=com.mysql.cj.jdbc.MysqlXADataSource 
  17. spring.jta.atomikos.datasource.secondary.unique-resource-name=test2 
  18. spring.jta.atomikos.datasource.secondary.max-pool-size=25 
  19. spring.jta.atomikos.datasource.secondary.min-pool-size=3 
  20. spring.jta.atomikos.datasource.secondary.max-lifetime=20000 
  21. spring.jta.atomikos.datasource.secondary.borrow-connection-timeout=10000 

创建多数据源配置类

  1. @Configuration 
  2. public class DataSourceConfiguration { 
  3.  
  4.     @Primary 
  5.     @Bean 
  6.     @ConfigurationProperties(prefix = "spring.jta.atomikos.datasource.primary"
  7.     public DataSource primaryDataSource() { 
  8.         return new AtomikosDataSourceBean(); 
  9.     } 
  10.  
  11.     @Bean 
  12.     @ConfigurationProperties(prefix = "spring.jta.atomikos.datasource.secondary"
  13.     public DataSource secondaryDataSource() { 
  14.         return new AtomikosDataSourceBean(); 
  15.     } 
  16.  
  17.     @Bean 
  18.     public JdbcTemplate primaryJdbcTemplate(@Qualifier("primaryDataSource") DataSource primaryDataSource) { 
  19.         return new JdbcTemplate(primaryDataSource); 
  20.     } 
  21.  
  22.     @Bean 
  23.     public JdbcTemplate secondaryJdbcTemplate(@Qualifier("secondaryDataSource") DataSource secondaryDataSource) { 
  24.         return new JdbcTemplate(secondaryDataSource); 
  25.     } 
  26.  

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

创建一个Service实现,模拟两种不同的情况。

  1. @Service 
  2. public class TestService { 
  3.  
  4.     private JdbcTemplate primaryJdbcTemplate; 
  5.     private JdbcTemplate secondaryJdbcTemplate; 
  6.  
  7.     public TestService(JdbcTemplate primaryJdbcTemplate, JdbcTemplate secondaryJdbcTemplate) { 
  8.         this.primaryJdbcTemplate = primaryJdbcTemplate; 
  9.         this.secondaryJdbcTemplate = secondaryJdbcTemplate; 
  10.     } 
  11.  
  12.     @Transactional 
  13.     public void tx() { 
  14.         // 修改test1库中的数据 
  15.         primaryJdbcTemplate.update("update user set age = ? where name = ?", 30, "aaa"); 
  16.         // 修改test2库中的数据 
  17.         secondaryJdbcTemplate.update("update user set age = ? where name = ?", 30, "aaa"); 
  18.     } 
  19.  
  20.     @Transactional 
  21.     public void tx2() { 
  22.         // 修改test1库中的数据 
  23.         primaryJdbcTemplate.update("update user set age = ? where name = ?", 40, "aaa"); 
  24.         // 模拟:修改test2库之前抛出异常 
  25.         throw new RuntimeException(); 
  26.     } 
  27.  

这里tx函数,是两句update操作,一般都会成功;而tx2函数中,我们人为的制造了一个异常,这个异常是在test1库中的数据更新后才产生的,这样就可以测试一下test1更新成功,之后是否还能在JTA的帮助下实现回滚。

创建测试类,编写测试用例

  1. @SpringBootTest(classes = Chapter312Application.class) 
  2. public class Chapter312ApplicationTests { 
  3.  
  4.     @Autowired 
  5.     protected JdbcTemplate primaryJdbcTemplate; 
  6.     @Autowired 
  7.     protected JdbcTemplate secondaryJdbcTemplate; 
  8.  
  9.     @Autowired 
  10.     private TestService testService; 
  11.  
  12.     @Test 
  13.     public void test1() throws Exception { 
  14.         // 正确更新的情况 
  15.         testService.tx(); 
  16.         Assertions.assertEquals(30, primaryJdbcTemplate.queryForObject("select age from user where name=?"Integer.class, "aaa")); 
  17.         Assertions.assertEquals(30, secondaryJdbcTemplate.queryForObject("select age from user where name=?"Integer.class, "aaa")); 
  18.     } 
  19.  
  20.     @Test 
  21.     public void test2() throws Exception { 
  22.         // 更新失败的情况 
  23.         try { 
  24.             testService.tx2(); 
  25.         } catch (Exception e) { 
  26.             e.printStackTrace(); 
  27.         } finally { 
  28.             // 部分更新失败,test1中的更新应该回滚 
  29.             Assertions.assertEquals(30, primaryJdbcTemplate.queryForObject("select age from user where name=?"Integer.class, "aaa")); 
  30.             Assertions.assertEquals(30, secondaryJdbcTemplate.queryForObject("select age from user where name=?"Integer.class, "aaa")); 
  31.         } 
  32.     } 
  33.  

这里有两个测试用例:

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

测试验证

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

 

 

 

 

观察一下启动阶段的日志,可以看到这些Atomikos初始化日志输出:

  1. 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 
  2. 2021-02-02 19:00:36.145  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.allow_subtransactions = true 
  3. 2021-02-02 19:00:36.145  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.recovery_delay = 10000 
  4. 2021-02-02 19:00:36.145  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.automatic_resource_registration = true 
  5. 2021-02-02 19:00:36.145  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.oltp_max_retries = 5 
  6. 2021-02-02 19:00:36.145  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.client_demarcation = false 
  7. 2021-02-02 19:00:36.145  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.threaded_2pc = false 
  8. 2021-02-02 19:00:36.145  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.serial_jta_transactions = true 
  9. 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 
  10. 2021-02-02 19:00:36.145  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.rmi_export_class = none 
  11. 2021-02-02 19:00:36.145  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.max_actives = 50 
  12. 2021-02-02 19:00:36.145  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.checkpoint_interval = 500 
  13. 2021-02-02 19:00:36.145  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.enable_logging = true 
  14. 2021-02-02 19:00:36.145  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.log_base_name = tmlog 
  15. 2021-02-02 19:00:36.146  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.max_timeout = 300000 
  16. 2021-02-02 19:00:36.146  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.trust_client_tm = false 
  17. 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 
  18. 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 
  19. 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 
  20. 2021-02-02 19:00:36.146  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.oltp_retry_interval = 10000 
  21. 2021-02-02 19:00:36.146  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : USING: java.naming.provider.url = rmi://localhost:1099 
  22. 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 
  23. 2021-02-02 19:00:36.146  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : USING: com.atomikos.icatch.default_jta_timeout = 10000 
  24. 2021-02-02 19:00:36.147  INFO 8868 --- [           main] c.a.icatch.provider.imp.AssemblerImp     : Using default (local) logging and recovery... 
  25. 2021-02-02 19:00:36.184  INFO 8868 --- [           main] c.a.d.xa.XATransactionalResource         : test1: refreshed XAResource 
  26. 2021-02-02 19:00:36.203  INFO 8868 --- [           main] c.a.d.xa.XATransactionalResource     

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

  1. {"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"}]} 
  2. {"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"}]} 
  3. {"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/

文章来源网络,作者:运维,如若转载,请注明出处:https://shuyeidc.com/wp/265153.html<

(0)
运维的头像运维
上一篇2025-05-05 08:55
下一篇 2025-05-05 08:57

相关推荐

  • 个人主题怎么制作?

    制作个人主题是一个将个人风格、兴趣或专业领域转化为视觉化或结构化内容的过程,无论是用于个人博客、作品集、社交媒体账号还是品牌形象,核心都是围绕“个人特色”展开,以下从定位、内容规划、视觉设计、技术实现四个维度,详细拆解制作个人主题的完整流程,明确主题定位:找到个人特色的核心主题定位是所有工作的起点,需要先回答……

    2025-11-20
    0
  • 社群营销管理关键是什么?

    社群营销的核心在于通过建立有温度、有价值、有归属感的社群,实现用户留存、转化和品牌传播,其管理需贯穿“目标定位-内容运营-用户互动-数据驱动-风险控制”全流程,以下从五个维度展开详细说明:明确社群定位与目标社群管理的首要任务是精准定位,需明确社群的核心价值(如行业交流、产品使用指导、兴趣分享等)、目标用户画像……

    2025-11-20
    0
  • 香港公司网站备案需要什么材料?

    香港公司进行网站备案是一个涉及多部门协调、流程相对严谨的过程,尤其需兼顾中国内地与香港两地的监管要求,由于香港公司注册地与中国内地不同,其网站若主要服务内地用户或使用内地服务器,需根据服务器位置、网站内容性质等,选择对应的备案路径(如工信部ICP备案或公安备案),以下从备案主体资格、流程步骤、材料准备、注意事项……

    2025-11-20
    0
  • 如何企业上云推广

    企业上云已成为数字化转型的核心战略,但推广过程中需结合行业特性、企业痛点与市场需求,构建系统性、多维度的推广体系,以下从市场定位、策略设计、执行落地及效果优化四个维度,详细拆解企业上云推广的实践路径,精准定位:明确目标企业与核心价值企业上云并非“一刀切”的方案,需先锁定目标客户群体,提炼差异化价值主张,客户分层……

    2025-11-20
    0
  • PS设计搜索框的实用技巧有哪些?

    在PS中设计一个美观且功能性的搜索框需要结合创意构思、视觉设计和用户体验考量,以下从设计思路、制作步骤、细节优化及交互预览等方面详细说明,帮助打造符合需求的搜索框,设计前的规划明确使用场景:根据网站或APP的整体风格确定搜索框的调性,例如极简风适合细线条和纯色,科技感适合渐变和发光效果,电商类则可能需要突出搜索……

    2025-11-20
    0

发表回复

您的邮箱地址不会被公开。必填项已用 * 标注