@Transactional跟@DS动态数据源注解冲突的解决

作者:林蜗牛snail 时间:2022-07-13 10:40:39 

@Transactional跟@DS动态数据源注解冲突

背景

前阵子写一个项目时,有个需求是要往3个库,3个表里插入数据,在同一个方法里,公司是用baomidou的@DS注解来实现配置动态数据源的。这是背景,然后呢,我在一个service方法里,就操作了这三张表,同时,我还加上了@Transactional注解,因为该方法是个save方法,我就加上了事务。

伪代码如下:


spring:
 datasource:
   dynamic:
     primary: A
     datasource:
       A:
         url:..
         driver-class-name: com.mysql.jdbc.Driver
         username: root
         password:
       B:
         url: ..
         driver-class-name: com.mysql.jdbc.Driver
         username: root
         password:
       C:
         url: ..
         driver-class-name: com.mysql.jdbc.Driver
         username: root
         password:

@Mapper
@DS("A")
public interface AMapper{
     @Insert("insert into a ...")
     void save();
}

@Mapper
@DS("B")
public interface BMapper{
     @Insert("insert into b ...")
     void save();
}

@Mapper
@DS("C")
public interface CMapper{
     @Insert("insert into c ...")
     void save();
}

public class aService{
@Autowired
private aMapper、bMapper、cMapper

@Transactional
   public void save(){
   aMapper.save();
   bMapper.save(); #报错
   cMapper.save();
   }    
}

在开发完成用postman自测时,发现bMapper.save()那一行报错了,报错内容:找不到b表。如果把@Transactional注释掉,代码正常运行,数据成功落库。我们明明在3个mapper上面都加了@DS注解来切换数据源,那为啥加了@Transactional就不行了呢?

@Transactional执行流程

  • save方法添加了 @Transactional 注解,Spring 事务就会生效。此时,Spring TransactionInterceptor 会通过 AOP 拦截该方法,创建事务。而创建事务,势必就会获得数据源。那么,TransactionInterceptor 会使用 Spring DataSourceTransactionManager 创建事务,并将事务信息通过 ThreadLocal 绑定在当前线程。


  • 而事务信息,就包括事务对应的 Connection 连接。那也就意味着,还没走到 OrderMapper 的查询操作,Connection 就已经被创建出来了。并且,因为事务信息会和当前线程绑定在一起,在 OrderMapper 在查询操作需要获得 Connection 时,就直接拿到当前线程绑定的 Connection ,而不是 OrderMapper 添加 @DS 注解所对应的 DataSource 所对应的 Connection 。

  • OK ,那么我们现在可以把问题聚焦到 DataSourceTransactionManager 是怎么获取 DataSource 从而获得 Connection 的了。对于每个 DataSourceTransactionManager 数据库事务管理器,创建时都会传入其需要管理的 DataSource 数据源。在使用 dynamic-datasource-spring-boot-starter 时,它创建了一个 DynamicRoutingDataSource ,传入到 DataSourceTransactionManager 中。

  • 而 DynamicRoutingDataSource 负责管理我们配置的多个数据源。例如说,本示例中就管理了 a、b、c 三个数据源,并且默认使用 a 数据源。那么在当前场景下,DynamicRoutingDataSource 需要基于 @DS 获得数据源名,从而获得对应的 DataSource ,结果因为我们在 Service 方法上,并没有添加 @DS 注解,所以它只好返回默认数据源,也就是 a 。故此,就发生了 找不到表 的异常。

我们在上面了解到,因为@Transactional会创建事务然后获得数据源,因为我们service方法上没有@DS注解,就拿了默认数据源,并且在这之后,这个事务信息会通过threadLocal跟当前线程绑定,事务信息包括了connection连接,也就意味着,在进入这个service方法的时候,当前事务就绑定了数据源a,在运行到bMapper.save()时,因为connection已经存在,所以拿到的数据源还是a,这时候就找不到b库里的表了。

解决方法

把这三个入库操作分为3个独立的方法,并且都加上@Transactional和 @DS注解(在service上加)。ps:在完成了aMapper.save()之后去调用bMapper.save()时,一定要把@Transactional设置为Propagation.REQUIRES_NEW,这样在调用另一个事务方法时,TransactionInterceptor 会将原事务挂起,暂时性的将原事务信息和当前线程解绑。

pps:

在一个事务方法里用this来调用另一个事务方法时,@DS也会起作用,原因是this调用的不是事务对象,不会开启事务。想具体了解可以看我之前发的这篇文章 //www.jb51.net/article/222082.htm

动态数据源切换失败

由事务@Transactional注解导致动态数据源切换失效的问题

不多BB,直接上代码:


public class DataSourceKey {
   /**
    * 用户数据源
    */
   public final static String USER = "userDataSource";
   /**
    * 报表数据源
    */
   public final static String REPORT = "reportDataSource";
   /**
    * 所有数据源的集合
    */
   final static List<String> SOURCES = ImmutableList.of(USER, REPORT);
   /**
    * 根据包名找到数据源, 多数据源的前缀不能存在相同的。 例: user -> userDataSource
    *
    * @param pack 包名
    * @return 数据源名
    */
   public static String getDataSourceKey(String pack) {
       return SOURCES.stream().filter(s -> s.startsWith(pack)).findFirst().orElse(USER);
   }
}

@Component
@Aspect
@Order(-1)
@Slf4j
public class DynamicDataSourceAspect {
   @Pointcut("execution(* com.in.g.data.mapper..*.*(..))")
   public void dataSourcePointcut() {
   }
   @Before("dataSourcePointcut()")
   public void doBefore(JoinPoint point) throws Throwable {
       log.debug("切换数据源开始。。。。。。。。。。。。");
       Package pack = point.getSignature().getDeclaringType().getPackage();
       String str = StringUtils.substringAfterLast(pack.getName(), ".");
       String dataSourceKey = DataSourceKey.getDataSourceKey(str);
       DynamicDataSourceHolder.set(dataSourceKey);
       log.debug("切换数据源成功,当前数据源:{}", dataSourceKey);
   }
   @After("dataSourcePointcut()")
   public void doAfterReturning() throws Throwable {
       DynamicDataSourceHolder.clear();
   }
}

/**
* 动态数据源持有者
*/
public class DynamicDataSourceHolder {
   public static ThreadLocal<String> keyHolder = new ThreadLocal<>();
   public static void clear() {
       keyHolder.remove();
   }
   public static void set(String key) {
       keyHolder.set(key);
   }
   public static String get() {
       return keyHolder.get();
   }
}

/**
* 动态数据源配置
*/
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
   @Override
   protected Object determineCurrentLookupKey() {
       return DynamicDataSourceHolder.get();
   }
}

//正确的代码   --这里偷懒了,直接controller调用dao层
@GetMapping("/testdb")
   public String testDateSources(){
//缩小事务的范围
      add();
      rptFieldMapper.selectxxxx();

return "sss";
   }
   @Transactional(rollbackFor = Exception.class)
   public void add() {
userMapper.insertXXXX(xxxx);
}

   //错误的代码,@Transactional注解会导致 数据源切换失败
   @GetMapping("/testdb")
   @Transactional(rollbackFor = Exception.class)
   public String testDateSources(){  
       userMapper.insertXXXX(xxxx);
       rptFieldMapper.selectXXXX();

return "sss";
   }

来源:https://blog.csdn.net/weixin_40378837/article/details/106554198

标签:@Transactional,注解,@DS,数据源
0
投稿

猜你喜欢

  • springboot的类加载器(org.springframework.boot.loader)过程详解

    2021-05-23 21:01:44
  • Spark内存调优指南

    2022-07-28 09:22:25
  • 深入了解Java核心类库--Math类

    2023-08-19 01:06:21
  • 举例讲解C#编程中对设计模式中的单例模式的运用

    2023-04-28 19:34:10
  • 深入探究Java线程与进程有哪些区别

    2023-05-06 13:26:03
  • java二维数组遍历的2种代码

    2022-05-03 08:52:50
  • 使用Spring Data Redis实现数据缓存的方法

    2021-08-02 10:19:25
  • C#实现数独解法

    2022-10-25 18:22:43
  • Java文件上传与文件下载实现方法详解

    2023-11-20 12:28:45
  • 最近较流行的效果 Android自定义View实现倾斜列表/图片

    2021-09-06 03:56:51
  • Spring Boot Maven Plugin打包异常解决方案

    2022-04-17 11:23:41
  • Springmvc拦截器执行顺序及各方法作用详解

    2023-06-10 08:11:46
  • Android实现退出时关闭所有Activity的方法

    2021-10-03 00:15:00
  • Android Rreact Native 常见错误总结

    2021-07-11 16:39:59
  • redis scan命令导致redis连接耗尽,线程上锁的解决

    2021-11-19 02:57:52
  • Java与C++实现相同的MD5加密算法简单实例

    2023-08-31 09:43:02
  • spring @Validated 注解开发中使用group分组校验的实现

    2021-09-29 14:07:22
  • Android开发教程之如何屏蔽View的重复点击

    2021-05-27 10:06:56
  • Android如何判断页面是否全屏

    2021-08-31 14:03:22
  • Echarts+SpringMvc显示后台实时数据

    2021-06-08 03:38:42
  • asp之家 软件编程 m.aspxhome.com