导读
- MyBatis多数据源配置与使用
- 其一
- 其二
- 1. 引依赖
- 2. 配置文件
- 3. 编写测试代码
- 4. 自定义DynamicDataSource类
- 5. DataSourceConfig配置类
- 6. AOP与ThreadLocal结合
- 7. 引入AOP依赖
- 8. DataSourceContextHolder
- 9. 自定义注解@UseDB
- 10. 创建切面类UseDBAspect
- 11. 修改DynamicDataSource
- 12. 简单测试一下
- 13. 未完
- 14. 结合栈的使用
- 15. 修改DataSourceContextHolder
- 16. 最后小坑
MyBatis多数据源配置与使用
前言:MyBatis默认情况下只能在application配置文件中配置单数据源,但有一些开发场景可能有多数据源的需求,这需要做一些额外的配置。
查了一下Mybatis多数据源的解决方案,主要有两种方式:
其一
利用MyBatis的@MapperScan注解,该注解除了标注扫描路径外,还能给扫描到的mapper文件的dao操作指定sqlSessionFactoryRef属性指定使用的SqlSessionFactory,此时我们就可以构建不同源的SqlSessionFactory,从而实现不同的mapper文件对应不同的数据源操作。
这种方式简单易懂,创建对应的SqlSessionFactory即可,缺点是需要为每个数据源维护对应的mapper文件。这里不详细描述这种方式。
其二
第二种方式是利用springboot自身的AbstractRoutingDataSource,AbstractRoutingDataSource是一个抽象类,其中维护了一个Map属性,该Map是用于存储多个数据源,通过不同的key获取对应的数据源。另外提供determineCurrentLookupKey抽象方法,供给用户自定义获取键的方式。例如我们两个数据库,db1和db2,当我们想用db1时,只需要让determineCurrentLookupKey方法获取到db1的key就行,db2同理。下面说下详细编码过程:
1. 引依赖
无需额外依赖,springboot,mybatis,mysql驱动即可,注意的是如果springboot版本过高,则可能需要升级其中的mybatis-spring版本,否则报错
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- springboot版本过高,需要升级其中的mybatis-spring版本,否则报错 --><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>3.0.2</version><exclusions><exclusion><groupId>org.mybatis</groupId><artifactId>mybatis-spring</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.mybatis</groupId><artifactId>mybatis-spring</artifactId><version>3.0.3</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.19</version></dependency></dependencies>
2. 配置文件
配置文件中定义数据源的信息,需要注意的是,在单数据源中,连接数据库参数时,使用的key是url,但在多数据源中,默认使用的是jdbc-url。(实际上我们也可以随便定义,但需要我们自己读取配置封装DataSource,后面会讲到)
spring:application:name: MultiSourceMyBatis# datasource配置文件如下datasource:# 数据源1db1:username: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.Driverjdbc-url: jdbc:mysql://127.0.0.1/inote?userUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai# 数据源2db2:username: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.Driverjdbc-url: jdbc:mysql://111.111.111.111/inote?userUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
3. 编写测试代码
测试代码的部分省略,就是controller,service,dao常规流程
4. 自定义DynamicDataSource类
创建DynamicDataSource类,继承AbstractRoutingDataSource类,实现determineCurrentLookupKey抽象方法,determineCurrentLookupKey方法就是如何获取DataSource的key的方法。通过不同的key获取对应的数据源。该方法的具体实现我们暂时留白,下面会再做修改
public class DynamicDataSource extends AbstractRoutingDataSource {/*** 获取数据源key的方式,要使用哪个数据源,是通过数据源key选择的,这个key是数据源map中的key*/@Overrideprotected Object determineCurrentLookupKey() {return "db1";}
}
5. DataSourceConfig配置类
DataSourceConfig这个类的主要作用是将我们自定义DynamicDataSource类的实例对象交由spring bean管理,由容器装配与调用。而在这之前,我们还需要给DynamicDataSource设置DataSource的map(也就是将多个DataSource添加到DynamicDataSource中)。
@Configuration
public class DataSourceConfig {@AutowiredEnvironment environment; // 用于读取application.yml文件配置/*** 构建两个数据库源,交由spring管理,但其实直接创建也无妨,注意保证创建相同配置的DataSource只有一个就行*/@Beanpublic DataSource db1(){HikariDataSource dataSource = new HikariDataSource();dataSource.setDriverClassName(environment.getProperty("spring.datasource.db1.driver-class-name"));dataSource.setJdbcUrl(environment.getProperty("spring.datasource.db1.jdbc-url"));dataSource.setUsername(environment.getProperty("spring.datasource.db1.username"));dataSource.setPassword(environment.getProperty("spring.datasource.db1.password"));return dataSource;}@Beanpublic DataSource db2(){HikariDataSource dataSource = new HikariDataSource();dataSource.setDriverClassName(environment.getProperty("spring.datasource.db2.driver-class-name"));dataSource.setJdbcUrl(environment.getProperty("spring.datasource.db2.jdbc-url"));dataSource.setUsername(environment.getProperty("spring.datasource.db2.username"));dataSource.setPassword(environment.getProperty("spring.datasource.db2.password"));return dataSource;}
// /**
// * 实际上创建DataSource的方式可以用以下代码替代,但是需要注意的是配置文件中的数据库连接参数要改为jdbc-url
// */
// @ConfigurationProperties(prefix = "spring.datasource.db1")
// @Bean
// public DataSource db1(){
// return DataSourceBuilder.create().build();
// }/*** 创建DynamicDataSource,并将db1,db2添加进去。*/@Bean("dynamicDataSource")@Primary // 该注解表示如果有多个相同bean,首选这个public DataSource dynamicDataSource(@Qualifier("db1") DataSource db1,@Qualifier("db2") DataSource db2){DynamicDataSource dynamicDataSource = new DynamicDataSource();//默认数据源,如果determineCurrentLookupKey方法获取到的key不在列表中,则走默认的datasourcedynamicDataSource.setDefaultTargetDataSource(db1);Map<Object,Object> map = new HashMap<>();map.put("db1",db1);map.put("db2",db2);dynamicDataSource.setTargetDataSources(map);return dynamicDataSource;}
}
至此,配置就完成了,此时我们可以通过上面的determineCurrentLookupKey方法指定我们想使用的数据源。
这时候就会有人问了,这也没完成啊,determineCurrentLookupKey方法中写死了数据库的key,怎么做到数据库切换?
刚才说了,determineCurrentLookupKey方法留白了,关键就是怎么动态切换要使用的数据库的key,就的改写determineCurrentLookupKey方法。下面就展开说说。
6. AOP与ThreadLocal结合
我们想实现多数据源,目的肯定是希望不同用户,或者不同操作同时进行时能够使用不同的数据库,而不是同一时刻只有一个数据源起作用,因而多线程下,相同操作对不同资源进行访问,首先想到的是ThreadLocal。如果在用户请求进来后,我们为其配置对应数据库源的key,然后在determineCurrentLookupKey中通过ThreadLocal获取到key,OK,万事大吉。
但……,我们给一个线程创建同一个数据源,我们需要怎么去创建,创建的时机是怎样的?基于编码习惯,我们肯定希望的是通过注解的方式做方法增强。
“对啊,AOP,ThreadLocal+AOP,在service层方法执行前捕获方法,然后通过ThreadLocal设置数据源,后续就能使用该数据源源进行sql操作了,你真聪明”。
7. 引入AOP依赖
<!-- aop依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>
8. DataSourceContextHolder
创建一个线程上下文工具类DataSourceContextHolder,该类主要作用是给线程创建ThreadLocal,然后实现ThreadLocal的getter,setter以及清除工作。
public class DataSourceContextHolder {private static ThreadLocal<String> dataSourceKey = new ThreadLocal<>();public static void setDataSourceKey(String key){dataSourceKey.set(key);}public static String getDataSourceKey(){return dataSourceKey.get();}public static void clear(){dataSourceKey.remove();}}
9. 自定义注解@UseDB
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseDB {/*** 要使用的数据源的key*/String value();
}
10. 创建切面类UseDBAspect
在代理方法执行前设置数据库源,方法执行后移除数据库源
@Aspect
@Component
public class UseDBAspect {/*** 定义切面*/@Pointcut(value = "@annotation(com.example.multisourcemybatis.announce.UseDB)")private void getAnnounce(){}/*** 环绕通知* @param joinPoint 切点,就是被注解的目标方法*/@Around("getAnnounce()")public Object logPostMapping(ProceedingJoinPoint joinPoint) throws Throwable {// 获取自定义注解中的value值MethodSignature signature = (MethodSignature) joinPoint.getSignature();UseDB annotation = signature.getMethod().getAnnotation(UseDB.class);String dataSourceKey = annotation.value();// 将dataSource的key设置到ThreadLocalDataSourceContextHolder.setDataSourceKey(dataSourceKey);// 执行目标方法,也就是service方法Object result = joinPoint.proceed();// 执行方法后,记得清除ThreadLocal,避免内存泄漏DataSourceContextHolder.clear();// 返回方法返回值return result;}}
11. 修改DynamicDataSource
补充DynamicDataSource的determineCurrentLookupKey方法,也就是如何获得key的方法,改为从ThreadLocal中获取即可
public class DynamicDataSource extends AbstractRoutingDataSource {/*** 获取数据源key的方式,要使用哪个数据源,是通过数据源key选择的,这个key是数据源map中的key*/@Overrideprotected Object determineCurrentLookupKey() {return DataSourceContextHolder.getDataSourceKey();}}
12. 简单测试一下
service方法
@UseDB("db1")public void addInDB1(UserInfo userInfo) {String stringId = SnowFlakeUtils.getStringId();userInfo.setId(stringId);userInfoMapper.insert(userInfo);}@UseDB("db2")public void addInDB2(UserInfo userInfo) {String stringId = SnowFlakeUtils.getStringId();userInfo.setId(stringId);userInfoMapper.insert(userInfo);}
controller方法
@PostMapping("add")public Result add(UserInfo userInfo) throws Exception {userInfoService.addInDB1(userInfo);userInfoService.addInDB2(userInfo);return ResultUtils.success();}
测试结果:
两个数据库分别插入一条数据,符合预期
13. 未完
“你这例子确实实现了通过注解方式实现数据源的切换,但是好像有点问题,你测试的例子是从controller中分别执行两个service方法(被自定义注解@UseDB标注的方法),但在实际开发中,我不确保总是从controller中调用,万一我在一个service中调用另一个service,而且在调用完另一个service后还需要进行数据库操作,这样的话就出问题了,在调用内层service的时候,我的ThreadLocal值已经被覆盖,并且内层service执行完后还进行了清除ThreadLocal,也就是说外层service设置的数据源已经没了,等到后面再执行dao操作时,会走默认的数据源,而不是@UseDB标注的数据源。这……是bug啊”
是的,理想状态下我们认为一个service不调用另一个service,但如果确实调用了,就可能出现bug,但也不是不能解决,那我们就针对性修改下吧
14. 结合栈的使用
我们要实现的效果是,外层方法使用外层数据源,内层方法使用内层方法数据源,如果还有内层的内层方法,使用内层的内层的数据源。然后方法执行完后一步一步弹出,但不影响相对外层的数据源。
有没有很熟悉,这就是栈啊,先进后出,我们使用栈来存储数据源的key,当调用内层方法后pop掉就行了,这样外层方法依旧能获取到外层的数据源key。
15. 修改DataSourceContextHolder
只修改DataSourceContextHolder,修改setter,getter以及clear方法,适配stack。
public class DataSourceContextHolder {private static ThreadLocal<Stack<String>> dataSourceKey = new ThreadLocal<>();/*** 将DataSource的key添加到ThreadLocal的Stack中,效果等同直接交给ThreadLocal* @param key DataSource的key*/public static void setDataSourceKey(String key){// 判断stack是否为空,在初始状态下stack == nullif (dataSourceKey.get()==null){dataSourceKey.set(new Stack<String>());}// 将DataSource的key添加到stack中dataSourceKey.get().push(key);}/*** 获取ThreadLocal中Stack最后添加进的key,效果等同获取当前DataSource的key* @return DataSource的key*/public static String getDataSourceKey(){// 注意,我们获取DataSource时不能采用pop方法,因为我们不能保证一个方法中只有一个数据库操作,// 如果直接pop,则会导致同一个方法后续数据库操作使用错误的数据源return dataSourceKey.get().peek();}/*** 将DataSource的key删除,但是不一定删除ThreadLocal,只有最后一个key配Stack踢出后才删除ThreadLocal*/public static void clear(){dataSourceKey.get().pop();// 如果此时栈中没有数据了,则将ThreadLocal清除if (dataSourceKey.get().empty()) {dataSourceKey.remove();}}/*** 额外再写个方法,无论如何都清除ThreadLocal,避免异常问题,没有将栈全部踢出,导致ThreadLocal内存泄漏* 建议在servlet拦截器中调用清除,afterCompletion中调用。*/public static void clearWhatever(){dataSourceKey.remove();}}
16. 最后小坑
这个不是上面代码的坑,而是AOP实现代理时,类的内部调用默认不走代理方法,也就是说,上面service的addInDB1和addInDB2方法,如果在addInDB1中直接调用或通过this调用addInDB2,如下
@UseDB("db1")public void addInDB1(UserInfo userInfo) {String stringId = SnowFlakeUtils.getStringId();userInfo.setId(stringId);userInfoMapper.insert(userInfo);// 直接调用addInDB2this.addInDB2(userInfo);}@UseDB("db2")public void addInDB2(UserInfo userInfo) {String stringId = SnowFlakeUtils.getStringId();userInfo.setId(stringId);userInfoMapper.insert(userInfo);}
上述代码中this.addInDB2(userInfo);
默认不走AOP动态代理,也就会导致addDB2方法用的依然是db1数据源这是不符合我们预期的,要解决这个问题,也就是走动态代理,我们要:
- 开启exposeProxy=true的配置,将类内部引用也走AOP代理
在启动类上标注
@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true) // 允许类内获取当前实例的代理
public class MultiSourceMyBatisApplication {public static void main(String[] args) {SpringApplication.run(MultiSourceMyBatisApplication.class, args);}}
- 获取代理对象,通过代理对象调用
@UseDB("db1")public void addInDB1(UserInfo userInfo) {String stringId = SnowFlakeUtils.getStringId();userInfo.setId(stringId);userInfoMapper.insert(userInfo);// 通过AopContext获取当前实例的代理对象UserInfoService userInfoService = (UserInfoService) AopContext.currentProxy();userInfoService.addInDB2(userInfo);}@UseDB("db2")public void addInDB2(UserInfo userInfo) {String stringId = SnowFlakeUtils.getStringId();userInfo.setId(stringId);userInfoMapper.insert(userInfo);}
至此全篇完。