一、简介
在项目开发过程中,经常需要连接多个数据源操作相关表和数据。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数据源。
原因分析:
- 在开启事务的同时,会从数据库连接池获取数据库连接,一个事务绑定一个连接,一旦绑定后,在整个事务的过程中,使用的数据库连接conn都是同一个
- Spring中@Transactional是通过APO+ThreadLocal实现的,再被拦截方法主体执行前会通过getConnection获取数据库连接conn保存到ThreadLocal中,然后在执行被拦截的事务方法中对数据进行CRUD时,会再次从 ThradLocal 获取之前创建的connection
- 如果某个方法没有使用@DS注解标识使用的数据源,会使用默认的数据源
- @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“异常。
解决办法:
- 既然事务开启后无法切换数据源, 那么可以在事务开启前就切换好数据源
- 一个事务只能关联一个数据源,那么可以开启2个事务,关联不同的的数据库
- @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注解存在以下几种无效的情况:
- 添加到mapper接口上无效。如前面例子中所示。
- 注解添加到service接口上
- 注解添加到service接口的方法上
以上三种情况中,注解都是无效的,方法执行时使用的都是默认的数据源,要想注解生效,必须将注解添加到接口实现类或者实现类的方法上。
当加入事务后,要在事务开始前就指定第一次连接的数据源,然后将数据源2中的操作用另外一个类封装成一个public方法,在该方法上使用@DS标志使用的数据源,同时添加事务注解并将事务传播机制设置为Propagation.REQUIRES_NEW。
注意:有事务就存在回滚,由于内部事务方法异常时,会造成外部事务回滚,但是外部事务异常并不会造成内部事务的回滚,因此配置了@Transactional(propagation = Propagation.REQUIRES_NEW)注解的方法调用,应放在业务最后方处理,从而保证事务回滚的一致性。