MyBatis多数据源配置与使用,基于ThreadLocal+AOP

news2024/11/16 0:00:38

导读

  • 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:
    # 数据源1
    db1:
      username: root
      password: root
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://127.0.0.1/inote?userUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
	# 数据源2
    db2:
      username: root
      password: root
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-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
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return "db1";
    }
}

5. DataSourceConfig配置类

DataSourceConfig这个类的主要作用是将我们自定义DynamicDataSource类的实例对象交由spring bean管理,由容器装配与调用。而在这之前,我们还需要给DynamicDataSource设置DataSource的map(也就是将多个DataSource添加到DynamicDataSource中)。

@Configuration
public class DataSourceConfig {

    @Autowired
    Environment environment;	// 用于读取application.yml文件配置

    /**
     * 构建两个数据库源,交由spring管理,但其实直接创建也无妨,注意保证创建相同配置的DataSource只有一个就行
     */
    @Bean
    public 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;
    }
    @Bean
    public 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不在列表中,则走默认的datasource
        dynamicDataSource.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设置到ThreadLocal
        DataSourceContextHolder.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
     */
    @Override
    protected 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 == null
        if (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);
        // 直接调用addInDB2
        this.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数据源这是不符合我们预期的,要解决这个问题,也就是走动态代理,我们要:

  1. 开启exposeProxy=true的配置,将类内部引用也走AOP代理

在启动类上标注

@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true)		// 允许类内获取当前实例的代理
public class MultiSourceMyBatisApplication {

    public static void main(String[] args) {
        SpringApplication.run(MultiSourceMyBatisApplication.class, args);
    }

}
  1. 获取代理对象,通过代理对象调用
    @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);
    }

至此全篇完。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1705367.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Spark Sql写代码方式(yarn)以及 spark sql整合hive详解

引入部分&#xff1a;通常我们在IDEA中写spark代码中如果设置了loacl参数&#xff0c;基本都是在IDEA本地运行&#xff0c;不会提交到 standalone或yarn上运行&#xff0c;在前几篇文章中写的大多数都是该种形式的spark代码&#xff0c;但也写到了如何将spark代码提交到standal…

如何防止锂电池反充

锂电池通常用于许多需要备用电源的设备应用中&#xff0c;例如实时时钟 (RTC) 和存储设备。当锂电池不是电路中的单一电源时&#xff0c;如果电池意外连接到可为电池充电的电源&#xff0c;则存在火灾或爆炸的风险。本应用笔记提供了在备用电源开关电路中连接锂电池所需的信息&…

springboot实现多开发环境匹配置

首先logbok-spring.xml里面的内容 <?xml version"1.0" encoding"UTF-8"?> <configuration><!-- 开发、测试环境 --><springProfile name"dev,test"><include resource"org/springframework/boot/logging/log…

MouseBoost Pro for Mac v3.4.7 鼠标右键助手 安装教程【支持M芯片】

MouseBoost Pro for Mac v3.4.7 鼠标右键助手 安装教程【支持M芯片】 原文地址&#xff1a;https://blog.csdn.net/weixin_48311847/article/details/139201501

Midjourney进阶篇 | 10大高阶风格,内附关键词

MJ绘画10大高阶风格&#xff0c;内附关键词&#xff01; 前言迪士尼风格 Disney style水彩画 watercolor paint波普艺术 pop art全息色彩风格 Holographic电影风格 film still二次元风格 Anime style赛博朋克风格 cyberpunk style极简主义风格 Minimalist design提示词&#xf…

【权威出版】2024年电气工程与传感检测技术国际会议(EESDT 2024)

2024年电气工程与传感检测技术国际会议 2024 International Conference on Electrical Engineering and Sensing Detection Technology 【1】会议简介 2024年电气工程与传感检测技术国际会议即将召开&#xff0c;这是一场汇聚全球电气工程与传感检测技术领域精英的学术盛会。 本…

832. 翻转图像 - 力扣

1. 题目 给定一个 n x n 的二进制矩阵 image &#xff0c;先 水平 翻转图像&#xff0c;然后 反转 图像并返回 结果 。 水平翻转图片就是将图片的每一行都进行翻转&#xff0c;即逆序。 例如&#xff0c;水平翻转 [1,1,0] 的结果是 [0,1,1]。 反转图片的意思是图片中的 0 全部被…

AT32F415 使用embedded studio笔记

由于Embedded Studio还没有支持雅特力的支持包&#xff0c;因此通过MDK的工程无法直接导入到embedded studio中。需要自己新建一个工程。 新建的时候是可以选择雅特力的单片机的。 工程新建好后&#xff0c;默认会包含3个文件。如下图所示。其中两个汇编文件的作用是之前MDK的…

Linux-----sed案例练习

1.数据准备 准备数据如下&#xff1a; [rootopenEuler ~]# cat openlab.txt My name is jock. I teach linux. I like play computer game. My qq is 24523452 My website is http://www.xianoupeng.com My website is http://www.xianoupeng.com My website is http://www.…

成都爱尔眼科巫雷院长教你在家“自查”白内障

检查以下自己&#xff08;或者父母、亲属&#xff09;是否有发生以下情况&#xff1a; 视物模糊视物模糊是白内障的主要症状。2、眼前暗影白内障早期&#xff0c;有的患者眼前会出现阴影&#xff0c;这是因为晶状体发生浑浊。晶状体混浊在眼前固定位置&#xff0c;患者会“看到…

PID传感器在光电显示行业VOC气体检测的应用

随着光电显示技术的飞速发展&#xff0c;液晶显示器等显示器件产品已经成为我们日常生活和工作中不可或缺的一部分。然而&#xff0c;在生产过程中&#xff0c;液晶显示器会释放大量的挥发性有机物&#xff08;VOC&#xff09;气体&#xff0c;对生产环境及工作人员的健康构成威…

Stable Diffusion 使用安装

Stable Diffusion是一个文本到图像的潜在扩散模型&#xff0c;由CompVis、Stability AI和LAION的研究人员和工程师创建。它使用来自LAION-5B数据库子集的512x512图像进行训练。使用这个模型&#xff0c;可以生成包括人脸在内的任何图像&#xff0c;因为有开源的预训练模型&…

【全开源】CMS内容管理系统源码(ThinkPHP+FastAdmin)

基于ThinkPHPFastAdmin的CMS内容管理系统&#xff0c;自定义内容模型、自定义单页、自定义表单、专题、统计报表、会员发布等 提供全部前后台无加密源代码和数据库私有化部署&#xff0c;UniAPP版本提供全部无加密UniAPP源码。 ​构建高效内容管理的基石 一、引言&#xff1a…

Unity 生成模版代码

1、创建模版代码文本 using System.Collections; using System.Collections.Generic; using UnityEngine;public class ClassNameScritpItem : MonoBehaviour {public GameObject go;// Start is called before the first frame updatevoid Start(){go new GameObject();}// …

充电宝哪家好用推荐?买什么充电宝性价比高?2024年充电宝排行榜

说实话&#xff0c;我其实是个手机重度使用者&#xff0c;买过的充电宝也有无数款了&#xff0c;每次手机没电的时候插座都离得不是特别近&#xff0c;不是要下床充电就是要固定在一个位置充电感觉怪麻烦的&#xff0c;但是有了充电宝后可以在床上玩手机都不用担心手机没电&…

李飞飞亲自撰文:大模型不存在主观感觉能力,多少亿参数都不行

近日&#xff0c;李飞飞连同斯坦福大学以人为本人工智能研究所 HAI 联合主任 John Etchemendy 教授联合撰写了一篇文章&#xff0c;文章对 AI 到底有没有感觉能力&#xff08;sentient&#xff09;进行了深入探讨。 「空间智能是人工智能拼图中的关键一环。」知名「AI 教母」李…

HAL工程应该这样搭建~

目录 一、为何要自己手动搭建一个软件工程&#xff1f; 二、在Keil5STM32F429**平台下的软件工程搭建 三、打开Keil MDK5软件创建一个新工程 四、在各个文件夹下添加所需文件 五、在MDK工程下添加文件 六、在MDK下添加路路径 一、为何要自己手动搭建一个软件工程&#xff…

010、哈希_命令

在Redis中&#xff0c;哈希类型是指键值本身又是一个键值对结构&#xff0c;形如value{{field1&#xff0c;value1}&#xff0c;…{fieldN&#xff0c;valueN}} 如图字符串和哈希类型对比 &#xff08;1&#xff09;设置值 hset key field value下面为user&#xff1a;1添加…

免费,Python蓝桥杯等级考试真题--第13级(含答案解析和代码)

Python蓝桥杯等级考试真题–第13级 一、 选择题 答案&#xff1a;C 解析&#xff1a;正向下标由0开始&#xff0c;下标3代表第四个元素&#xff0c;故答案为C。 答案&#xff1a;A 解析&#xff1a;range&#xff08;0,4&#xff09;的取前不取后&#xff0c;元组的符号是小括…

【运维自动化-配置平台】如何跨业务转移主机

在如何创建业务拓扑中&#xff0c;了解到业务是蓝鲸体系重要的资源管理纬度&#xff0c;主机在业务之前需要流转怎么做呢&#xff1f;比如要把A业务一台主机划给B业务使用权限中心 跨业务转移主机一般场景是由源主机所在业务的负责人发起&#xff0c;需要申请目标业务的相关权…