Spring事务管理(二)分布式事务管理之JTA与链式事务

本文涉及的产品
RDS AI 助手,专业版
RDS MySQL DuckDB 分析主实例,集群系列 4核8GB
RDS MySQL DuckDB 分析主实例,基础系列 4核8GB
简介: Spring事务管理(二)分布式事务管理之JTA与链式事务

什么是分布式事务


跨库的事务就属于分布式事务,比如对两个库的不同表同时修改和同时rollback等。

上一节中,我们只是演示了单个库(数据源)的事务处理。这一节主要讲如何处理多个数据源的事务。


为什么多数据源下不能使用普通事务来处理呢?


我想很多人都有这个问题,打个比方,分库分表后有个数据库A和数据库B,A中有抢票记录,B中有票数记录。当我们完成抢票功能,需要在B减少票数的同时在A中增加记录。但是如果有下面的代码发生:


  1. @Transactional
  2. publicvoid multiDBTX(){
  3.    B.reduce(ticketId);
  4.    if(true){
  5.        thrownewRuntimeException("throw new exception");
  6.    }
  7.    A.save(result);
  8. }


我在B扣除票数后抛出异常,然后执行A库添加记录。


如果没有分布式事务处理,则结果就是B票数扣除,但A没有保存记录。也就是出错后B并没有进行事务回滚。


那问题来了,怎么才能实现我们的要求呢。


分布式事务原则


CAP定理


web无法同时满足以下三点:

  1. 一致性: 所有数据变动都是同步的
  2. 可用性: 每个操作都必须有预期的响应
  3. 分区容错性: 出现单个节点无法可用,系统依然正常对外提供服务

BASE理论


BASE理论是对CAP中的一致性和可用性进行一个权衡的结果。核心思想是即使无法做到强一致性,但可以使用一些技术手段达到最终一致。

  1. Basically Available(基本可用):允许系统发生故障时,损失一部分可用性。
  2. Soft state(软状态):允许数据同步存在延迟。
  3. Eventually consistent(最终一致性): 不需要保持强一致性,最终一致即可。


那如何来实现分布式事务管理呢?


分布式事务管理实践


1. JTA实现


事务有效的屏蔽了底层事务资源,使应用可以以透明的方式参入到事务处理中,但是与本地事务相比,XA 协议的系统开销大


在这里我先带大家走出一个误解,你在网上搜JTA一般都是分布式事务用它,但是它就是用来做分布式事务的吗?不是的,我在上文说过,JTA只是Java实现XA事务的一个规范,我们在第一节 Spring事务管理(一)快速入门中用到的事务,都可以叫JTA事务管理。下面主要说JTA实现分布式事务管理:


这里我们会用到Atomikos事务管理器,它是一个开源的事务管理器,实现了XA的一种分布式事务处理并可以嵌入到你的SpringBoot当中。



拓展:什么是XA


基本上所有的数据库都会支持XA事务,百度百科上说法:XA协议由Tuxedo首先提出的,并交给X/Open组织,作为资源管理器(数据库)与事务管理器的接口标准。简单的说,它是事务的标准,JTA也是它标准的java实现。


1.1 导入pom



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

1.2 设置数据源


SpringBoot设置多数据源这里只说下思路(重点还是说事务实现):


  1. application.yml中配置多数据源配置。
  2. 写配置类加载配置并放入DataSource并设置事务:

  1. @Configuration
  2. @DependsOn("transactionManager")
  3. @EnableJpaRepositories(basePackages ="com.fantj.repository.user", entityManagerFactoryRef ="userEntityManager", transactionManagerRef ="transactionManager")
  4. @EnableConfigurationProperties(UserDatasourceProperties.class)
  5. publicclassUserConfig{

  6.    @Autowired
  7.    privateJpaVendorAdapter jpaVendorAdapter;

  8.    // 这里注入 dataSource信息的类
  9.    @Autowired
  10.    privateUserDatasourceProperties userDatasourceProperties;

  11.    @Bean(name ="userDataSource")
  12.    publicDataSource userDataSource(){
  13.        // 给XADataSource 设置 DataSource 属性
  14.        MysqlXADataSource mysqlXaDataSource =newMysqlXADataSource();
  15.        mysqlXaDataSource.setURL(userDatasourceProperties.getUrl());
  16.        mysqlXaDataSource.setUser(userDatasourceProperties.getUser());
  17.        mysqlXaDataSource.setPassword(userDatasourceProperties.getPassword());
  18.        mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true);
  19.        // 创建 Atomiko, 并将 mysql的XA交给JTA管理
  20.        AtomikosDataSourceBean xaDataSource =newAtomikosDataSourceBean();
  21.        xaDataSource.setXaDataSource(mysqlXaDataSource);
  22.        // 设置唯一资源名
  23.        xaDataSource.setUniqueResourceName("datasource2");
  24.        return xaDataSource;
  25.    }

  26.    @Bean(name ="userEntityManager")
  27.    @DependsOn("transactionManager")
  28.    publicLocalContainerEntityManagerFactoryBean userEntityManager()throwsThrowable{

  29.        HashMap<String,Object> properties =newHashMap<String,Object>();
  30.        properties.put("hibernate.transaction.jta.platform",AtomikosJtaPlatform.class.getName());
  31.        properties.put("javax.persistence.transactionType","JTA");

  32.        LocalContainerEntityManagerFactoryBean entityManager =newLocalContainerEntityManagerFactoryBean();
  33.        // 给工厂bean设置 资源加载属性
  34.        entityManager.setJtaDataSource(userDataSource());
  35.        entityManager.setJpaVendorAdapter(jpaVendorAdapter);
  36.        entityManager.setPackagesToScan("com.fantj.pojo.user");
  37.        entityManager.setPersistenceUnitName("userPersistenceUnit");
  38.        entityManager.setJpaPropertyMap(properties);
  39.        return entityManager;
  40.    }

  41. }


这只是一个数据源的配置,第二个数据源的配置也类似,注意不能同Entity同Repository,映射放在不同包下实现。 两个都返回


LocalContainerEntityManagerFactoryBean它便会交给@Transaction去管理,两个数据源配置完后。这样的代码B将会回滚。


  1. @Transactional
  2. publicvoid multiDBTX(){
  3.    B.reduce(ticketId);
  4.    if(true){
  5.        thrownewRuntimeException("throw new exception");
  6.    }
  7.    A.save(result);
  8. }


1.3 JTA缺点


因为JTA采用两阶段提交方式,第一次是预备阶段,第二次才是正式提交。当第一次提交出现错误,则整个事务出现回滚,一个事务的时间可能会较长,因为它要跨越多个 数据库 多个数据资源的的操作,所以在性能上可能会造成吞吐量低。而且,它只能用在单个服务内。一个完善的JTA事务还需要同时考虑很多元素,这只是个示例。


2. 链式事务管理


链式事务就是声明一个ChainedTransactionManager 将所有的数据源事务按顺序放到该对象中,则事务会按相反的顺序来执行事务。


网上发现了一个链式事务管理的处理顺序,总结的很到位。


  1. 1.start message transaction
  2. 2.receive message
  3. 3.start database transaction
  4. 4.update database
  5. 5.commit database transaction
  6. 6.commit message transaction   ##当这一步出现错误时,上面的因为已经commit,所以不会rollback


可以看到,从345可以看到,它后拿到的事务先提交,这就导致如果1出错,则不会进行数据回滚。跟Spring的同步事务差不多,同步事务也是这种特性。

下面我会测试这个性质。


为了方便,我拿JdbcTemplate来测试该事务。


DBConfig.java


配置DataSource以及返回Template新实例和链式事务配置。


  1. /**
  2. * DB配置类
  3. */
  4. @Configuration
  5. publicclassDBConfig{
  6.    /**
  7.     * user-DB配置
  8.     */
  9.    @Bean
  10.    @Primary
  11.    @ConfigurationProperties(prefix ="spring.datasource.user")
  12.    publicDataSourceProperties userDataSourceProperties(){
  13.        returnnewDataSourceProperties();
  14.    }

  15.    @Bean
  16.    @Primary
  17.    publicDataSource userDataSource(){
  18.        return userDataSourceProperties().initializeDataSourceBuilder().type(HikariDataSource.class).build();
  19.    }

  20.    @Bean
  21.    publicJdbcTemplate userJdbcTemplate(@Qualifier("userDataSource")DataSource userDataSource){
  22.        returnnewJdbcTemplate(userDataSource);
  23.    }
  24.    /**
  25.     * result-DB配置
  26.     */
  27.    @Bean
  28.    @ConfigurationProperties(prefix ="spring.datasource.result")
  29.    publicDataSourceProperties resultDataSourceProperties(){
  30.        returnnewDataSourceProperties();
  31.    }

  32.    @Bean
  33.    publicDataSource resultDataSource(){
  34.        return resultDataSourceProperties().initializeDataSourceBuilder().type(HikariDataSource.class).build();
  35.    }

  36.    @Bean
  37.    publicJdbcTemplate resultJdbcTemplate(@Qualifier("resultDataSource")DataSource resultDataSource){
  38.        returnnewJdbcTemplate(resultDataSource);
  39.    }
  40.    /**
  41.     * 链式事务配置
  42.     */
  43.    @Bean
  44.    publicPlatformTransactionManager transactionManager(){
  45.        DataSourceTransactionManager userTM =newDataSourceTransactionManager(userDataSource());
  46.        DataSourceTransactionManager resultTM =newDataSourceTransactionManager(resultDataSource());
  47.        returnnewChainedTransactionManager(userTM,resultTM);
  48.    }
  49. }


transactionManager()方法实现了链式事务配置,注意我放置的顺序先userTM后resultTM,所以事务应该是先拿到 userTM然后拿到 resultTM然后提交 resultTM最后提交 userTM,也就是说,如果我在提交user事务的时候出错,此时result相关的事务已经提交完成,所以result数据是不能回滚的。


2.1 测试


  1. @RequestMapping("")
  2. @Transactional
  3. publicvoid testTX(){
  4. //        resultJdbcTemplate.execute("insert into result values(68,6,6)");
  5.    userJdbcTemplate.execute("insert into user values (6,'FantJ',23,'男')");
  6.    if(true){
  7.        thrownewRuntimeException("yes , throw one exception");
  8.    }
  9.    resultJdbcTemplate.execute("insert into result values(66,6,6)");
  10. //        userJdbcTemplate.execute("insert into user values (8,'FantJ',23,'男')");
  11. }


两个数据库没有内容。


控制台精简后的日志:


  1. Creatingnew transaction with name [springbootjtamultidb.jtamulti.JtaMultiApplicationTests.testTX]:
  2. Creatingnew transaction with name [springbootjtamultidb.jtamulti.JtaMultiApplicationTests.testTX]:
  3. Began transaction (1)for test context [DefaultTestContext@1dde4cb2 testClass =...
  4. Executing SQL statement [insert into user values (6,'FantJ',23,'男')]
  5. Initiating transaction rollback
  6. Rolling back JDBC transaction on Connection[HikariProxyConnection@318550723 wrapping com.mysql.cj.jdbc.ConnectionImpl@57bd6a8f]
  7. Releasing JDBC Connection[HikariProxyConnection@318550723 wrapping com.mysql.cj.jdbc.
  8. Initiating transaction rollback
  9. Rolling back JDBC transaction on Connection[HikariProxyConnection@1201991394 wrapping com.mysql.cj.jdbc.ConnectionImpl@36f6e521]
  10. Releasing JDBC Connection[HikariProxyConnection@1201991394 wrapping com.mysql.cj.jdbc.ConnectionImpl@36f6e521] after transaction
  11. Resuming suspended transaction after completion of inner transaction
  12. Rolled back transaction for test:[DefaultTestContext@1dde4cb2 testClass =JtaMultiApplicationTests, testInstance = springbootjtamultidb.jtamulti.JtaMultiApplicationTests@441772e,
image.png
网络异常,图片无法展示
|
网络异常,图片无法展示
|
其实只要是两个dao操作中间出错或者第一个dao操作之前出错,事务都能正常回滚。如果result操作再前,user操作再后,user操作完抛出异常,也能回滚事务,原因上文有讲。
  1. @RequestMapping("")
  2. @Transactional
  3. publicvoid testTX(){
  4.    resultJdbcTemplate.execute("insert into result values(68,6,6)");
  5. //        userJdbcTemplate.execute("insert into user values (6,'FantJ',23,'男')");
  6. //        if (true){
  7. //            throw new RuntimeException("yes , throw one exception");
  8. //        }
  9. //        resultJdbcTemplate.execute("insert into result values(66,6,6)");
  10.    userJdbcTemplate.execute("insert into user values (8,'FantJ',23,'男')");
  11.    if(true){
  12.        thrownewRuntimeException("yes , throw one exception");
  13.    }
  14. }

这段代码也能正常回滚,结果我就不贴了。(浪费大家精力)


2.2 验证第二个事务不能回滚的情况


重要的事情再重复一遍:注意我放置的顺序先userTM后resultTM,所以事务应该是先拿到 userTM然后拿到 resultTM然后提交 resultTM最后提交 userTM,也就是说,如果我在提交user事务的时候出错,此时result相关的事务已经提交完成,所以result数据是不能回滚的。


  1. 代码和之前的一样,需要在事务提交的方法中打断点
  2. @RequestMapping("")
  3. @Transactional
  4. publicvoid testTX(){
  5.    resultJdbcTemplate.execute("insert into result values(68,6,6)");
  6.    userJdbcTemplate.execute("insert into user values (8,'FantJ',23,'男')");
  7. }


网络异常,图片无法展示
|
网络异常,图片无法展示
|
网络异常,图片无法展示
|
网络异常,图片无法展示
|
注意拦截到断点时,先放行一个commit,也就是result事务的commit,然后拦截到第二个commit请求时,关闭user所在的数据库,然后放行。

下面是将第一个commit请求放行后的控制台日志:

  1. Creatingnew transaction with name [springbootjtamultidb.jtamulti.controller.MainController.
  2. AcquiredConnection[HikariProxyConnection@810768078 wrapping com.mysql.cj.jdbc.ConnectionImpl@3cfb22cd]for JDBC transaction
  3. Switching JDBC Connection[HikariProxyConnection@810768078 wrapping com.mysql.cj.jdbc.ConnectionImpl@3cfb22cd] to manual commit
  4. Executing SQL statement [insert into result values(68,6,6)]
  5. Executing SQL statement [insert into user values (8,'FantJ',23,'男')]
  6. Initiating transaction commit
  7. Committing JDBC transaction on Connection[HikariProxyConnection@810768078 wrapping com.mysql.cj.jdbc.ConnectionImpl@3cfb22cd]
  8. Releasing JDBC Connection[HikariProxyConnection@810768078 wrapping com.mysql.cj.jdbc.ConnectionImpl@3cfb22cd] after transaction
  9. Resuming suspended transaction after completion of inner transaction


注意倒数第三行日志,它出现证明我们第一个result的sql事务已经提交,此时你刷新数据库数据已经更新了,但是我们断点并还没有放行,user的事务还没有提交,我把user的数据库源关闭,再放行,可以看到,result已经有数据,user没有数据,此时result并没有进行回滚,这是链式事务的缺点。

网络异常,图片无法展示
|
网络异常,图片无法展示
|
网络异常,图片无法展示
|
网络异常,图片无法展示
|

谈谈使用环境


JTA优缺点


JTA它的缺点是二次提交,事务时间长,数据锁的时间太长,性能比较低。

优点是强一致性,对于数据一致性要求很强的业务很有利,而且可以用于微服务。


链式/同步事务优缺点


优点: 比JTA轻量,能满足大部分事务需求,也是强一致性。


缺点: 只能单机玩,不能用于微服务,事务依次提交后提交的事务若出错不能回滚。


它两的比较


  1. JTA重,Chained轻。


  1. JTA能用于微服务分布式事务,Chained只能用于单机分布式事务。


事实上我们处理分布式事务都要求做到最终一致性。就是你刚开始我不需要保持你的数据一致,你中间可以出错,但是我能保证最终数据是一致的。这种做法性能最高,下一章节会谈。

网络异常,图片无法展示
|
相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。 &nbsp; 相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情:&nbsp;https://www.aliyun.com/product/rds/mysql&nbsp;
相关文章
|
8月前
|
人工智能 Java Nacos
基于 Spring AI Alibaba + Nacos 的分布式 Multi-Agent 构建指南
本文将针对 Spring AI Alibaba + Nacos 的分布式多智能体构建方案展开介绍,同时结合 Demo 说明快速开发方法与实际效果。
5258 99
|
10月前
|
监控 Java API
Spring Boot 3.2 结合 Spring Cloud 微服务架构实操指南 现代分布式应用系统构建实战教程
Spring Boot 3.2 + Spring Cloud 2023.0 微服务架构实践摘要 本文基于Spring Boot 3.2.5和Spring Cloud 2023.0.1最新稳定版本,演示现代微服务架构的构建过程。主要内容包括: 技术栈选择:采用Spring Cloud Netflix Eureka 4.1.0作为服务注册中心,Resilience4j 2.1.0替代Hystrix实现熔断机制,配合OpenFeign和Gateway等组件。 核心实操步骤: 搭建Eureka注册中心服务 构建商品
1438 3
|
9月前
|
存储 安全 Java
管理 Spring 微服务中的分布式会话
在微服务架构中,管理分布式会话是确保用户体验一致性和系统可扩展性的关键挑战。本文探讨了在 Spring 框架下实现分布式会话管理的多种方法,包括集中式会话存储和客户端会话存储(如 Cookie),并分析了它们的优缺点。同时,文章还涵盖了与分布式会话相关的安全考虑,如数据加密、令牌验证、安全 Cookie 政策以及服务间身份验证。此外,文中强调了分布式会话在提升系统可扩展性、增强可用性、实现数据一致性及优化资源利用方面的显著优势。通过合理选择会话管理策略,结合 Spring 提供的强大工具,开发人员可以在保证系统鲁棒性的同时,提供无缝的用户体验。
198 0
|
9月前
|
SQL Java 关系型数据库
Spring事务传播机制:7种姿势教你玩转"事务接力赛"
事务传播机制是Spring框架中用于管理事务行为的重要概念,它决定了在方法调用时事务如何传递与执行。通过7种传播行为,开发者可以灵活控制事务边界,适应不同业务场景。例如:REQUIRED默认加入或新建事务,REQUIRES_NEW独立开启新事务,NESTED支持嵌套回滚等。合理使用传播机制不仅能保障数据一致性,还能提升系统性能与健壮性。掌握这“七种人格”,才能在复杂业务中游刃有余。
|
8月前
|
监控 Java 数据库连接
《深入理解Spring》事务管理——数据一致性的守护者
Spring事务管理确保数据一致性,支持声明式与编程式两种方式。通过@Transactional注解简化配置,提供传播行为、隔离级别、回滚规则等灵活控制,结合ACID特性保障业务逻辑可靠执行。
|
8月前
|
负载均衡 Java API
《深入理解Spring》Spring Cloud 构建分布式系统的微服务全家桶
Spring Cloud为微服务架构提供一站式解决方案,涵盖服务注册、配置管理、负载均衡、熔断限流等核心功能,助力开发者构建高可用、易扩展的分布式系统,并持续向云原生演进。
|
Java 关系型数据库 数据库
微服务——SpringBoot使用归纳——Spring Boot事务配置管理——常见问题总结
本文总结了Spring Boot中使用事务的常见问题,虽然通过`@Transactional`注解可以轻松实现事务管理,但在实际项目中仍有许多潜在坑点。文章详细分析了三个典型问题:1) 异常未被捕获导致事务未回滚,需明确指定`rollbackFor`属性;2) 异常被try-catch“吃掉”,应避免在事务方法中直接处理异常;3) 事务范围与锁范围不一致引发并发问题,建议调整锁策略以覆盖事务范围。这些问题看似简单,但一旦发生,排查难度较大,因此开发时需格外留意。最后,文章提供了课程源代码下载地址,供读者实践参考。
424 0
|
Java 关系型数据库 数据库
微服务——SpringBoot使用归纳——Spring Boot事务配置管理——Spring Boot 事务配置
本文介绍了 Spring Boot 中的事务配置与使用方法。首先需要导入 MySQL 依赖,Spring Boot 会自动注入 `DataSourceTransactionManager`,无需额外配置即可通过 `@Transactional` 注解实现事务管理。接着通过创建一个用户插入功能的示例,展示了如何在 Service 层手动抛出异常以测试事务回滚机制。测试结果表明,数据库中未新增记录,证明事务已成功回滚。此过程简单高效,适合日常开发需求。
1886 0
|
Java 数据库 微服务
微服务——SpringBoot使用归纳——Spring Boot事务配置管理——事务相关
本文介绍Spring Boot事务配置管理,阐述事务在企业应用开发中的重要性。事务确保数据操作可靠,任一异常均可回滚至初始状态,如转账、购票等场景需全流程执行成功才算完成。同时,事务管理在Spring Boot的service层广泛应用,但根据实际需求也可能存在无需事务的情况,例如独立数据插入操作。
352 0
|
10月前
|
Java 关系型数据库 数据库
深度剖析【Spring】事务:万字详解,彻底掌握传播机制与事务原理
在Java开发中,Spring框架通过事务管理机制,帮我们轻松实现了这种“承诺”。它不仅封装了底层复杂的事务控制逻辑(比如手动开启、提交、回滚事务),还提供了灵活的配置方式,让开发者能专注于业务逻辑,而不用纠结于事务细节。
1197 1