mybaitis-plus使用事务导致多数据源切换失败

一、简介

        在项目开发过程中,经常需要连接多个数据源操作相关表和数据。Mybatis-Plus提供了一种多数据源动态切换的方式,即mybatis-plus-dynamic,通过简单的注解@DS即可实现数据源的动态切换。

        最近在项目中,使用Mybatis-Plus的多数据源编写代码,由于引入了事务操作,导致@DS注解失效,数据源切换失败,记录一下解决过程。

二、使用介绍

        首先介绍一下mybatis-plus-dynamic的使用,为后面的问题复现打下基础。

1、pom.xml引入依赖

首先在pom文件中引入dynamic-datasource-spring-boot-starter依赖包

2、配置数据源连接信息

        配置文件分为两种,推荐使用的是yml格式,如下所示。如果在yml文件中配置了数据源连接信息后,在properties类型的配置文件中就无需再配置数据库连接信息了,会默认去yml文件中读取的。

        如果不想使用yml格式,也可以在poperties文件格式中进行配置,如下所示,其格式与yml略微有点不同。

        上述配置文件表示配置了两个数据源的连接,一个是到ctm01favorite_db数据库的连接(简称master),一个是到icpc_icpcdb数据库的连接(简称icpc),其中默认数据源是master。

3、使用@DS注解切换数据源

        @DS可以配置在类或者方法上,也可以不加,不同情况的结果如下表所示。

注解

位置

结果

不加@DS

默认数据源

加@DS(“dbname”)

类上

类中所有方法都使用该数据源

加@DS(“dbname”)

方法上

就近原则,方法上注解优于类上注解

 三、实际使用

        基于前面的配置,我们在项目中创建两个mapper类分别指向对应的数据库,在mapper上使用@DS注解标注每一个mapper分别属于的数据源,在master主数据源中创建PlaceEventStatMapper,使用@DS(“master”)标志。

        在数据源二icpc中创建PlaceEventMapper,使用@DS(“icpc“)标志。

        注意此处是将@DS注解加在mapper接口上的,接着我们复现一下问题:新建一个类,编写一个定时任务,先从icpc数据库的place_event表中count出相关的数据,然后insert到master数据库的place_event_stat表中,并对该定时任务方法添加事务注解@Transactional(rollbackFor = Exception.class)。

        理想情况下:按照mybatis-plus多数据源自动切换原理,当执行placeEventMapper.count时,由于PlaceEventMapper上使用@DS(“icpc”)标志会连接icpc数据源,执行查询操作,然后执行placeEventStatMapper.insert时,由于PlaceEventStatMapper上使用@DS(“master”)标志会自动切换连接master数据源,进行insert插入操作。

        实际情况下:启动项目,等待定时任务执行,开启数据库日志为Debug模式,可以看到如下日志:

        当执行select count(id) from place_event语句时候抛出了“ERROR: relation "place_event" does not exist“异常,即表place_event表不存在,这说明此时连接的并不是icpc数据源,而是master数据源。

    原因分析:

  1. 在开启事务的同时,会从数据库连接池获取数据库连接,一个事务绑定一个连接,一旦绑定后,在整个事务的过程中,使用的数据库连接conn都是同一个
  2. Spring中@Transactional是通过APO+ThreadLocal实现的,再被拦截方法主体执行前会通过getConnection获取数据库连接conn保存到ThreadLocal中,然后在执行被拦截的事务方法中对数据进行CRUD时,会再次从 ThradLocal 获取之前创建的connection
  3. 如果某个方法没有使用@DS注解标识使用的数据源,会使用默认的数据源
  4. @DS注解添加在mapper接口上无效

        通过上面的分析可知:在我们的示例代码的run方法运行时,首先由于存在@Transactional注解会执行事务切入,该方法上没有显示使用@DS注解标识使用的数据源,且mapper接口上的注解无效,因此使用默认的master数据源,那么此时run方法的事务会关联一个到master的连接conn,并将该conn保存到ThreadLocal中,接着进入方法体,执行placeEventMapper.count操作时,重新拿到ThreadLocal中的数据库连接conn,可见此时拿到的就只是master数据库的连接,该库中本来就没有place_event表,所以抛出了relation "place_event" does not exist“异常。

解决办法:

  1. 既然事务开启后无法切换数据源, 那么可以在事务开启前就切换好数据源
  2. 一个事务只能关联一个数据源,那么可以开启2个事务,关联不同的的数据库
  3. @DS加在mapper接口上无效,可直接加在方法上

        Spring中@Transactional注解默认的事务传播属性是Propagation.REQUIRED,即如果当前存在事务就加入该事务,如果不存在就创建一个新事物。同时提供了另外一个事务传播属性是propagation = Propagation.REQUIRES_NEW,即无论当前有没有事务,都开启一个新事务,如果当前存在事务就把当前事务挂起,因此通过对propagation的配置就可以实现(2)中的想法。

        通过上面的分析,我们可以在run方法上显示加上@DS(“icpc)标识,然后将master数据库中的placeEventStatMapper.insert方法封装起来,在该方法上添加@Transactional(propagation = Propagation.REQUIRES_NEW),表示执行该方法时新建一个事务,同时加上@DS(“master”),表示该方法连接的是master数据源。

改造后的代码如下所示

        按照理想情况下,应该执行成功,但是启动项目日志显示relation "place_event_stat" does not exist异常,也就是现在连接的是icpc数据源,而master数据源连不上了。这说明run方法上的@DS(“icpc”)注解生效了,但是insert方法上的两个配置均未生效,没开启多个事务,也没连上master数据源。

继续分析

        Spring是使用AOP来代理事务控制的,而AOP基于动态代理实现, @Transactional写在一个方法上时,这个方法将会被spring动态代理,生成一个动态代理类,对原方法进行修饰增强,但是要注意,只有生成的代理类才有事务,原来的类并没有。而同一个类中两个方法之间的调用,其调用者都是这个类,而不是代理类,因此事务是不会生效的,相当于只是调用了本类中的一个普通方法而已。

        通过spring事务原理的分析可知,insert方法和run方法在同一个类中,即使insert方法上有@Transactional注解,这个事务也不会生效,事务不生效那么传播机制当然也不生效。

继续修改:

        既然一个类中调用事务不生效,那么可以使用两个类实现。因此只需将insert方法换一个类调用即可,新建一个PlaceEventStatService类将insert方法放入,如下所示。

然后在run方法中使用service调用insert方法

 

        启动项目,发现定时任务执行成功。说明insert方法上的@DS注解和@Transactional注解都生效了。

        通过前面的问题复现,可以看出当加了事务以后,很多情况下会使@DS注解失效。要保证数据源切换正常,事务使用也正常,就必须替换数据库连接,也就是改变事务的传播机制,使其产生新的事务,获取新的数据库连接

四、总结

        在不加事务的时候,@DS注解也存在无效的几种情况,比如前面我们将注解加在mapper接口上,发现也是无效的。

@DS注解存在以下几种无效的情况:

  1. 添加到mapper接口上无效。如前面例子中所示。
  2. 注解添加到service接口上
  3. 注解添加到service接口的方法上

        以上三种情况中,注解都是无效的,方法执行时使用的都是默认的数据源,要想注解生效,必须将注解添加到接口实现类或者实现类的方法上。

        当加入事务后,要在事务开始前就指定第一次连接的数据源,然后将数据源2中的操作用另外一个类封装成一个public方法,在该方法上使用@DS标志使用的数据源,同时添加事务注解并将事务传播机制设置为Propagation.REQUIRES_NEW。

        注意:有事务就存在回滚,由于内部事务方法异常时,会造成外部事务回滚,但是外部事务异常并不会造成内部事务的回滚,因此配置了@Transactional(propagation = Propagation.REQUIRES_NEW)注解的方法调用,应放在业务最后方处理,从而保证事务回滚的一致性。

  • 31
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值