@Transactional和@DS避免数据源冲突及保证原子性的解决方案(提供gitee源码)

前言:

在最近的实际工作当中,遇到一个业务场景,需要保证新老数据库的同步,在动态切换数据库当中,遇到了@Transactional和@DS的冲突问题,这里我写了一份个人的总结,从业务还原、原因剖析和如何解决等等一步步阐明。

目录

一、场景模拟

二、原因分析

三、解决方案

(一)更改事务的传播机制

(二)@DSTransactional注解代替@Transactional

四、事务回滚机制

(一)、在Master和Slave事务执行前抛出异常

(二)、当master事务和slave事务中间抛出异常

(三)、在slave方法中抛出异常

(四)、在master和slave事务之后

(五)、临时添加一个temp数据库,进行插入操作,并抛出异常

(六)、嵌套

(七)、使用@DSTransactional注解,在slave和temp之间抛出异常

(八)、使用@DSTransactional注解,在最后抛出异常

五、gitee源码地址

六、总结


一、场景模拟

1、先导入pom.xml依赖


   
   
  1. <dependencies>
  2. <dependency>
  3. <groupId>org.springframework.boot </groupId>
  4. <artifactId>spring-boot-starter-jdbc </artifactId>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.springframework.boot </groupId>
  8. <artifactId>spring-boot-starter-web </artifactId>
  9. </dependency>
  10. <dependency>
  11. <groupId>org.springframework.boot </groupId>
  12. <artifactId>spring-boot-starter-test </artifactId>
  13. <scope>test </scope>
  14. </dependency>
  15. <!--数据源切换依赖-->
  16. <dependency>
  17. <groupId>com.baomidou </groupId>
  18. <artifactId>dynamic-datasource-spring-boot-starter </artifactId>
  19. <version>3.5.2 </version>
  20. </dependency>
  21. <!--MySQL驱动依赖-->
  22. <dependency>
  23. <groupId>mysql </groupId>
  24. <artifactId>mysql-connector-java </artifactId>
  25. <scope>runtime </scope>
  26. </dependency>
  27. <!--Mybatis依赖-->
  28. <dependency>
  29. <groupId>org.mybatis.spring.boot </groupId>
  30. <artifactId>mybatis-spring-boot-starter </artifactId>
  31. <version>2.2.2 </version>
  32. </dependency>
  33. </dependencies>

2、yml文件配置了3个数据源,主数据源是master,从数据源是slave,后续临时加了个数据源temp,为了用于事务的测试,数据库均为MySQL。


   
   
  1. server:
  2. port: 8080
  3. spring:
  4. datasource:
  5. dynamic:
  6. primary: master
  7. datasource:
  8. master:
  9. username: root
  10. password: root
  11. url: jdbc:mysql://localhost:3306/master?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
  12. driver-class-name: com.mysql.cj.jdbc.Driver
  13. slave:
  14. username: root
  15. password: root
  16. url: jdbc:mysql://localhost:3306/slave?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
  17. driver-class-name: com.mysql.cj.jdbc.Driver
  18. temp:
  19. username: root
  20. password: root
  21. url: jdbc:mysql://localhost:3306/temp?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
  22. driver-class-name: com.mysql.cj.jdbc.Driver
  23. mybatis:
  24. mapper-locations: classpath:mapping/*.xml

3、分别编写主数据源和从数据源的Mapper层接口


   
   
  1. @Mapper
  2. @DS("master")
  3. public interface MasterMapper {
  4. int insertUser (User user);
  5. }

   
   
  1. @Mapper
  2. @DS("slave")
  3. public interface SlaveMapper {
  4. int insertRole (Role role);
  5. }

4、分别编写对应的XML文件


   
   
  1. <?xml version="1.0" encoding="UTF-8" ?>
  2. <!DOCTYPE mapper
  3. PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  4. "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  5. <mapper namespace="com.example.demo.mapper.MasterMapper">
  6. <insert id="insertUser" parameterType="com.example.demo.bean.User">
  7. INSERT INTO user (username, password)
  8. VALUES(#{username}, #{password})
  9. </insert>
  10. </mapper>

   
   
  1. <?xml version="1.0" encoding="UTF-8" ?>
  2. <!DOCTYPE mapper
  3. PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  4. "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  5. <mapper namespace="com.example.demo.mapper.SlaveMapper">
  6. <insert id="insertRole" parameterType="com.example.demo.bean.Role">
  7. INSERT INTO role (role)
  8. VALUES(#{role})
  9. </insert>
  10. </mapper>

5、编写Service方法


   
   
  1. @Service
  2. public class UserService {
  3. @Resource
  4. private MasterMapper masterMapper;
  5. @Resource
  6. private SlaveMapper slaveMapper;
  7. @Transactional
  8. public void Add (){
  9. User user = new User();
  10. user.setUsername( "张三");
  11. user.setPassword( "123456");
  12. masterMapper.insertUser(user);
  13. Role role = new Role();
  14. role.setRole( "管理员");
  15. slaveMapper.insertRole(role);
  16. }
  17. }

6、测试运行,报错如下

### Error updating database.  Cause: java.sql.SQLSyntaxErrorException: Table 'master.role' doesn't exist
### The error may exist in file [D:\JavaProjects\Java\demo\target\classes\mapping\SlaveMapper.xml]
### The error may involve com.example.demo.mapper.SlaveMapper.insertRole-Inline
### The error occurred while setting parameters
### SQL: INSERT INTO role (role)         VALUES(?)
### Cause: java.sql.SQLSyntaxErrorException: Table 'master.role' doesn't exist
; bad SQL grammar []; nested exception is java.sql.SQLSyntaxErrorException: Table 'master.role' doesn't exist

从报错的表面上来看,是因为在主数据库当中不存在role这个表,可是我们已经切换了数据源呀,可为什么还是报错呢,具体的详细原因,我们往下细说。

二、原因分析

@Transactional开启事务的时候,会先从数据库连接池获取是数据库的连接(基于Spring的AOP切面),我们UserService方法上面没有打上@DS注解,所以Spring默认采用的是主数据源,而且在这之后,这个事务会通过ThreadLocal跟当前线程绑定并也报错了connection连接,通俗的来讲,在进入UserService方法的时候,当前事务以及绑定了数据源Master,在运行到SlaveMapper接口时,因为当前事务的connection连接已经存在,所以拿到的数据源还是默认的Master,于是想找到Slave当中的role表,当然是不可能的,所以只能报错了。

三、解决方案

(一)更改事务的传播机制

其实我们只要更改一下事务的传播机制,将它设置为:Propagation.REQUIRES_NEW即可,意思就是将原有的Spring事务挂起,并创建一个新的事务并分配的一个新的connection,两者不影响,具体操作如下:

1、修改原有的UserService代码

不用通过@DS指定数据源,因为默认是Master;将slave业务操作分离出来,封装到一个Service服务类当中,再通过@Resource注解注入进来,最后还是指定一下回滚策略,遇到异常就回滚。


   
   
  1. @Service
  2. public class UserService {
  3. @Resource
  4. private MasterMapper masterMapper;
  5. @Resource
  6. private SlaveService slaveService;
  7. @Transactional(rollbackFor = Exception.class)
  8. public void Add (){
  9. User user = new User();
  10. user.setUsername( "张三");
  11. user.setPassword( "123456");
  12. masterMapper.insertUser(user);
  13. slaveService.slave();
  14. }
  15. }

2、 编写SlaveService服务代码

必须通过@DS指定一下数据源为slave,在slave方法上面重新修改一下事务的传播机制即可


   
   
  1. @Service
  2. @DS("slave")
  3. public class SlaveService {
  4. @Resource
  5. private SlaveMapper slaveMapper;
  6. @Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
  7. public void slave (){
  8. Role role = new Role();
  9. role.setRole( "管理员");
  10. slaveMapper.insertRole(role);
  11. }
  12. }

3、其他的保持不变,最后我们再测试一下,看一下输出结果,成功了!

(二)@DSTransactional注解代替@Transactional

我们可以使用@DSTransactional注解代替@Transactional即可,其他什么都不用动,也是最简单的方法。

1、导入pom.xml依赖


   
   
  1. <dependency>
  2. <groupId>com.baomidou </groupId>
  3. <artifactId>dynamic-datasource-spring-boot-starter </artifactId>
  4. <version>3.5.0 </version>
  5. </dependency>

2、修改UserService代码


   
   
  1. @Service
  2. public class UserService {
  3. @Resource
  4. private MasterMapper masterMapper;
  5. @Resource
  6. private SlaveMapper slaveMapper;
  7. @DSTransactional
  8. public void Add (){
  9. User user = new User();
  10. user.setUsername( "张三");
  11. user.setPassword( "123456");
  12. masterMapper.insertUser(user);
  13. Role role = new Role();
  14. role.setRole( "管理员");
  15. slaveMapper.insertRole(role);
  16. }
  17. }

3、运行测试,查看输出结果,成功!

四、事务回滚机制

(一)、在Master和Slave事务执行前抛出异常

UserService类:


   
   
  1. @Transactional(rollbackFor = Exception.class)
  2. public void Add (){
  3. User user = new User();
  4. user.setUsername( "张三");
  5. user.setPassword( "123456");
  6. int a = 1/ 0;
  7. masterMapper.insertUser(user);
  8. slaveService.slave();
  9. }

结果:数据保持一致

(二)、当master事务和slave事务中间抛出异常

UserService类:


   
   
  1. @Transactional(rollbackFor = Exception.class)
  2. public void Add (){
  3. User user = new User();
  4. user.setUsername( "张三");
  5. user.setPassword( "123456");
  6. masterMapper.insertUser(user);
  7. int a = 1/ 0;
  8. slaveService.slave();
  9. }

结果:回滚master事务,slave事务无影响

(三)、在slave方法中抛出异常

SlaveService类:


   
   
  1. @Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
  2. public void slave (){
  3. Role role = new Role();
  4. role.setRole( "管理员");
  5. slaveMapper.insertRole(role);
  6. int a = 1/ 0;
  7. }

结果:master和slave事务都会进行回滚

(四)、在master和slave事务之后

UserService类:


   
   
  1. @Transactional(rollbackFor = Exception.class)
  2. public void Add (){
  3. User user = new User();
  4. user.setUsername( "张三");
  5. user.setPassword( "123456");
  6. masterMapper.insertUser(user);
  7. slaveService.slave();
  8. int a = 1/ 0;
  9. }

结果:master事务回滚,slave已经提交事务,入库

(五)、临时添加一个temp数据库,进行插入操作,并抛出异常

UserService类:


   
   
  1. @Transactional(rollbackFor = Exception.class)
  2. public void Add (){
  3. User user = new User();
  4. user.setUsername( "张三");
  5. user.setPassword( "123456");
  6. masterMapper.insertUser(user);
  7. slaveService.slave();
  8. tempService.temp();
  9. }

TempService类:


   
   
  1. @Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
  2. public void temp (){
  3. Car car = new Car();
  4. car.setCar( "AE86");
  5. tempMapper.insertCar(car);
  6. int a = 1/ 0;
  7. }

结果:master回滚,slave事务提交,temp回滚

(六)、嵌套

UserService类:


   
   
  1. @Transactional(rollbackFor = Exception.class)
  2. public void Add (){
  3. User user = new User();
  4. user.setUsername( "张三");
  5. user.setPassword( "123456");
  6. masterMapper.insertUser(user);
  7. slaveService.slave();
  8. }

SlaveService类: 


   
   
  1. @Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
  2. public void slave (){
  3. Role role = new Role();
  4. role.setRole( "管理员");
  5. slaveMapper.insertRole(role);
  6. tempService.temp();
  7. }

TempService类: 


   
   
  1. @Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
  2. public void temp (){
  3. Car car = new Car();
  4. car.setCar( "AE86");
  5. tempMapper.insertCar(car);
  6. int a = 1/ 0;
  7. }

 结果:master回滚,slave回滚,temp回滚

(七)、使用@DSTransactional注解,在slave和temp之间抛出异常

UserService类


   
   
  1. @DSTransactional
  2. public void Add (){
  3. User user = new User();
  4. user.setUsername( "张三");
  5. user.setPassword( "123456");
  6. masterMapper.insertUser(user);
  7. Role role = new Role();
  8. role.setRole( "管理员");
  9. slaveMapper.insertRole(role);
  10. int a= 1/ 0;
  11. Car car = new Car();
  12. car.setCar( "AE86");
  13. tempMapper.insertCar(car);
  14. }

结果:master回滚,slave回滚、temp回滚

(八)、使用@DSTransactional注解,在最后抛出异常


   
   
  1. @DSTransactional
  2. public void Add (){
  3. User user = new User();
  4. user.setUsername( "张三");
  5. user.setPassword( "123456");
  6. masterMapper.insertUser(user);
  7. Role role = new Role();
  8. role.setRole( "管理员");
  9. slaveMapper.insertRole(role);
  10. Car car = new Car();
  11. car.setCar( "AE86");
  12. tempMapper.insertCar(car);
  13. int a= 1/ 0;
  14. }

结果:同上 

五、gitee源码地址

https://gitee.com/huang-tuantuan/transactional

六、总结

以上就是我目前对于这种问题的分析与解决方案,参考了网上各种各样的解决方案,写了一份总结文档。

版权声明:本文为CSDN博主「黄团团」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接

通过此篇文章了解到,普通的数据源切换中,事务失效的原因;
多数据源中,可在事务中开启新事物,但是由于事务的传播性,导致外部事物无法影响内部事务,但是内部事务会直接影响外部事务;
可以依然怒多数据源的事务来解决类似问题,既: @DSTransactional,这样就达成了既定的目标,事务一致性;与单数据源事务,基本没有区别;

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
@Transactional注解是Spring框架提供的一个事务管理注解,用于标识一个方法或类需要进行事务管理。它的作用是将带有@Transactional注解的方法或类包装在一个事务中,确保方法执行过程中的数据库操作要么全部成功要么全部回滚。然而,@Transactional注解本身并不负责切换数据源的功能,它只是负责事务管理的逻辑。 要切换数据源,您可以使用其他适合您的方式,例如使用Spring的AbstractRoutingDataSource或者使用第三方的数据源切换库,如DynamicDataSource等。 在同时使用@Transactional和切换数据源的情况下,切换数据源可能无效的原因如下: 1. @Transactional注解的执行顺序在切换数据源之前,这意味着在事务开始之前,数据源已经被锁定了,导致无法切换数据源。因此,在使用@Transactional注解时切换数据源可能无效。 2. 在执行完@Transactional注解锁定数据源之后,@DS切面才会执行。然而,@DS切面在切换数据源时只是改变了缓存数据源配置的key字符串,并没有重新根据当前字符串的key去获取最新的数据源。因此,在使用@DS切面切换数据源时,如果没有重新获取最新的数据源,可能会导致数据源未切换的现象。 为了解决@Transactional不能切换数据源的问题,您可以尝试以下方法: 1. 可以尝试修改执行顺序,将切换数据源的逻辑放在@Transactional注解之前,确保切换数据源生效。 2. 可以尝试使用其他方法切换数据源,如使用Spring的AbstractRoutingDataSource,它允许您根据某些规则动态选择数据源。 3. 可以尝试使用第三方的数据源切换库,如DynamicDataSource,它提供了更灵活的数据源切换方式。 请根据您的具体需求选择适合的方式来实现@Transactional和切换数据源的需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值