MyBatis Plus 插件 动态数据源实现原理与源码讲解 (dynamic-datasource-spring-boot-starter-master)
作者:mmseoamin日期:2023-12-20

目录

1. 介绍

2. 基本原理

3. 源码介绍

3.1 使用 AOP 拦截,方法执行前获取到当前方法要用的数据源

3.2 实现自定义 DataSource 接口,实现 DataSource 接口的 getConnect 方法做动态处理


1. 介绍

多数据源即一个项目中同时存在多个不同的数据库连接池。

比如 127.0.0.1:3306/test   127.0.0.1:3307/test 127.0.0.1:3308/test

总之项目存在需要操作多个库的需求。

具体在编码方面呢,具体就是一个service 中,方法1使用库1查询,方法2使用库2查询。

2. 基本原理

多数据源实现原理是什么呢?可分为两大关键部分

1. 使用 AOP 拦截,方法执行前获取到当前方法要用的数据源

可以使用自定义注解实现,注解参数带数据源名称,然后自己解析

2. 实现自定义 DataSource 接口,实现 DataSource 接口的 getConnect 方法做动态处理

动态处理,就是拿到 AOP 那一步获取到的数据源,直接返回该数据源

基本原理,可看这个简易图

MyBatis Plus 插件 动态数据源实现原理与源码讲解 (dynamic-datasource-spring-boot-starter-master),第1张

3. 源码介绍

源码地址 https://gitee.com/baomidou/dynamic-datasource-spring-boot-starter

(源码一定要自己完整看一遍,此篇博客只展示部分关键源码)

3.1 使用 AOP 拦截,方法执行前获取到当前方法要用的数据源

@DS 注解代表定义当前方法、当前类使用哪个数据源

MyBatis Plus 插件 动态数据源实现原理与源码讲解 (dynamic-datasource-spring-boot-starter-master),第2张

 value 指定当前类、方法使用的数据源名称

数据源名称也是在配置文件中定义的 

MyBatis Plus 插件 动态数据源实现原理与源码讲解 (dynamic-datasource-spring-boot-starter-master),第3张

注解处理切面 DynamicDataSourceAnnotationAdvisor

MyBatis Plus 插件 动态数据源实现原理与源码讲解 (dynamic-datasource-spring-boot-starter-master),第4张

切面 advice 由外部传过来,要处理的注解也从外面传过来。

也就是这里,这行代码的意思是

DynamicDataSourceAnnotationInterceptor 负责处理 DS 注解

MyBatis Plus 插件 动态数据源实现原理与源码讲解 (dynamic-datasource-spring-boot-starter-master),第5张

 接着看 DynamicDataSourceAnnotationInterceptor 如何处理

MyBatis Plus 插件 动态数据源实现原理与源码讲解 (dynamic-datasource-spring-boot-starter-master),第6张

下面分别解释下

1. 将 @DS 注解的 value 值压入 ThreadLocal 当前线程的栈

看这段方法 DynamicDataSourceContextHolder.push(dsKey);

ThreadLocal 中存储的是 Deque 类,也就是一个双端队列(两头都可以插入的队列) ,使用的是 ArrayDeque 双端队列,内部是一个数组。

为什么使用队列,而不是简单一个字符串,注释已经写的很清楚了,看注释即可。

MyBatis Plus 插件 动态数据源实现原理与源码讲解 (dynamic-datasource-spring-boot-starter-master),第7张

 ArrayDeque 的 push 就是在队列首部添加一个元素。MyBatis Plus 插件 动态数据源实现原理与源码讲解 (dynamic-datasource-spring-boot-starter-master),第8张

2. 调用实际的方法

这里不是切面吗,实际方法也就是被拦截的方法。也就是直接调用业务逻辑。

3. 将第 1 步中的元素弹出来

业务逻辑执行完成后,就将刚才加入的元素弹出来。

其实这里很像 JVM 虚拟机栈,方法调用就是压入栈,方法结束调用就是出栈,栈顶就是当前执行的方法。

而此处这里的栈顶就代表 当前正在执行的方法所用的 数据源名称。而方法执行完了,这里也该出栈了。

这里核心逻辑已经完了,本质就是这么简单。

3.2 实现自定义 DataSource 接口,实现 DataSource 接口的 getConnect 方法做动态处理

DynamicRoutingDataSource 代表动态路由数据源

做的事情就是运行时动态路由出一个当前需要的数据源。

接着看源码,首先与 SpringBoot 整合时 自动配置出当前 Bean

MyBatis Plus 插件 动态数据源实现原理与源码讲解 (dynamic-datasource-spring-boot-starter-master),第9张

DynamicRoutingDataSource 类图

MyBatis Plus 插件 动态数据源实现原理与源码讲解 (dynamic-datasource-spring-boot-starter-master),第10张

3.2.1 DataSource

代表一个数据源,由javax 扩展定义

3.2.2 AbstractDataSource

抽象实现,将一些对数据源的配置操作都实现为不支持操作抛出异常 UnsupportedOperationException

(动态数据源相当于一个代理,不需要给动态数据源本身设置相关配置)

3.2.3 AbstractRoutingDataSource

抽象实现,路由动态配置源,实现了关键方法 getConnection ,完成了路由操作

看看源码  getConnection()

getConnection() 何时调用呢,也就是上一步的切面中的第 2 步中,invocation.proceed(),执行业务逻辑的过程中,遇到的 数据库层的操作时,就会到这里了。

MyBatis Plus 插件 动态数据源实现原理与源码讲解 (dynamic-datasource-spring-boot-starter-master),第11张

这里直接看简单的非事务的获取数据源这里。

关键代码 determineDataSource().getConnection()

这个方法由子类实现,也就是下面的 DynamicRoutingDataSource 类

MyBatis Plus 插件 动态数据源实现原理与源码讲解 (dynamic-datasource-spring-boot-starter-master),第12张

3.2.4 DynamicRoutingDataSource

动态路由数据源核心实现,完成 数据源的维护(添加删除数据源)、数据源的选择

接着上面的源码流程,子类的 determineDataSource 方法最终调用了 getDataSource

MyBatis Plus 插件 动态数据源实现原理与源码讲解 (dynamic-datasource-spring-boot-starter-master),第13张

 getDataSource 源码如下

/**
 * 获取数据源
 *
 * @param ds 数据源名称
 * @return 数据源
 */
public DataSource getDataSource(String ds) {
    if (StringUtils.isEmpty(ds)) {
        // 没有指定数据源名称,直接使用默认的数据源
        return determinePrimaryDataSource();
    } else if (!groupDataSources.isEmpty() && groupDataSources.containsKey(ds)) {
        // 从分组数据源中找一个数据源
        return groupDataSources.get(ds).determineDataSource();
    } else if (dataSourceMap.containsKey(ds)) {
        // 直接根据名称找一个数据源
        return dataSourceMap.get(ds);
    }
    if (strict) {
        // 开启了严格模式时,如果没有找到数据源,就抛出异常
        throw new CannotFindDataSourceException("dynamic-datasource could not find a datasource named" + ds);
    }
    // 使用默认的数据源
    return determinePrimaryDataSource();
}

下面分别讲解关键之处

1. 没有指定数据源名称,直接使用默认的数据源

没有指定代表的是没有加 @DS 注解,或者加了注解,但是 value 值没有写

此时就是用默认的数据源,默认的数据源是什么呢?

也就是配置文件中的 primary 中指定的数据源名称,如果不配置的话默认值就是 master

MyBatis Plus 插件 动态数据源实现原理与源码讲解 (dynamic-datasource-spring-boot-starter-master),第14张

2. 实现自定义 DataSource 接口,实现 DataSource 接口的 getConnect 方法做动态处理

从分组找一个数据源 groupDataSources.get(ds).determineDataSource();

分组是什么意思?

分组定义的规则是 group_xxx,也就是数据源名称以下划线分割,下划线前面的就是组名。

分组的作用是什么呢?本质用于实现一个名称对应多数据库源。

比如一主多从,可以将从数据源都分到 slave 组里面,用的时候就是 @DS("slave") // 组名

在实际决定数据源的时候,就会按照一定的策略从这个组里的数据源挑选一个了。

接着看源码,如何 从分组数据源中找一个数据源

groupDataSources.get(ds).determineDataSource();

MyBatis Plus 插件 动态数据源实现原理与源码讲解 (dynamic-datasource-spring-boot-starter-master),第15张

 最后到了策略的选择,DynamicDataSourceStrategy

MyBatis Plus 插件 动态数据源实现原理与源码讲解 (dynamic-datasource-spring-boot-starter-master),第16张

DynamicDataSourceStrategy 有两个实现类

LoadBalanceDynamicDataSourceStrategy 负载均衡动态数据源策略

看源码,这个就是按顺序一个个选择下来,达到负载均衡方式

MyBatis Plus 插件 动态数据源实现原理与源码讲解 (dynamic-datasource-spring-boot-starter-master),第17张

RandomDynamicDataSourceStrategy 随机动态数据源策略

这个完全就是纯随机选一个

MyBatis Plus 插件 动态数据源实现原理与源码讲解 (dynamic-datasource-spring-boot-starter-master),第18张

3. 直接根据名称找一个数据源 

如果走到了这里,说明这个数据源名称没有配置分组,那就直接根据名称取这单个数据源了

直接纯 get 了

MyBatis Plus 插件 动态数据源实现原理与源码讲解 (dynamic-datasource-spring-boot-starter-master),第19张

数据源何时初始化的

还是在 DynamicRoutingDataSource,这个类实现了 Spring InitializingBean 

接口回调方法 afterPropertiesSet,当当前 Bean 内部的属性都初始化完毕了后就回调这个方法

MyBatis Plus 插件 动态数据源实现原理与源码讲解 (dynamic-datasource-spring-boot-starter-master),第20张

 看看  afterPropertiesSet 回调方法内容

MyBatis Plus 插件 动态数据源实现原理与源码讲解 (dynamic-datasource-spring-boot-starter-master),第21张

 这里只看关键代码

1. dataSources.putAll(provider.loadDataSources());

@Autowired private List providers; providers 是什么呢 ? 

providers 代表 动态数据源配置的来源,默认实现就是从 yml 中来,也就是 SpringBoot 的 application.yml 配置

默认实现

MyBatis Plus 插件 动态数据源实现原理与源码讲解 (dynamic-datasource-spring-boot-starter-master),第22张

传进去的参数配置类

MyBatis Plus 插件 动态数据源实现原理与源码讲解 (dynamic-datasource-spring-boot-starter-master),第23张

DynamicDataSourceProvider 也就是解析了这些配置 来获取到所有配置

拿到配置后,就要解析这些配置了 ,这里委托了父类处理

MyBatis Plus 插件 动态数据源实现原理与源码讲解 (dynamic-datasource-spring-boot-starter-master),第24张

这里完成创建数据源,然后将结果封装成了 Map dataSourceMap 返回

(泛型为 <数据源名称,数据源实例>)

MyBatis Plus 插件 动态数据源实现原理与源码讲解 (dynamic-datasource-spring-boot-starter-master),第25张

 看看如何创建数据源的 defaultDataSourceCreator.createDataSource(dataSourceProperty)

大致流程如下:

MyBatis Plus 插件 动态数据源实现原理与源码讲解 (dynamic-datasource-spring-boot-starter-master),第26张

这里介绍一下 creators

dynamic-datasource-creator 模块下定义了单独数据源创建的代码

DataSourceCreator 代表一个数据源创建器,用于创建一个数据源。

MyBatis Plus 插件 动态数据源实现原理与源码讲解 (dynamic-datasource-spring-boot-starter-master),第27张

 每种数据源类型都有自己的创建器,比如这里常见的 Druid、Hikar

MyBatis Plus 插件 动态数据源实现原理与源码讲解 (dynamic-datasource-spring-boot-starter-master),第28张

这里就举例其中一个 HikariDataSourceCreator,其他的都差不多

HikariDataSourceCreator

MyBatis Plus 插件 动态数据源实现原理与源码讲解 (dynamic-datasource-spring-boot-starter-master),第29张

调用这些创造器的创建的时候默认直接就启动了,除非配置了懒加载。

到现在,数据源就已经创建完了。再次说一下这是在Spring 的 afterPropertiesSet 回调里完成创建的。(afterPropertiesSet  即当前 Bean 的所有属性 Spring 都填充完毕后回调)

2. addDataSource(dsItem.getKey(), dsItem.getValue());

上一步的 provider.loadDataSources() 讲解完毕了,这次看看下面的 addDataSource(dsItem.getKey(), dsItem.getValue());

MyBatis Plus 插件 动态数据源实现原理与源码讲解 (dynamic-datasource-spring-boot-starter-master),第30张

 addDataSource 方法

MyBatis Plus 插件 动态数据源实现原理与源码讲解 (dynamic-datasource-spring-boot-starter-master),第31张

 首先是先给 dataSourceMap 放进去了。这里会返回旧的数据源(如果是第一次加入,则返回null),所以下面判断了如果返回有值旧关闭掉旧的数据源,关闭就是调用数据源的 close 方法。

然后是 addGroupDataSource

MyBatis Plus 插件 动态数据源实现原理与源码讲解 (dynamic-datasource-spring-boot-starter-master),第32张

这里数据源就完成了添加,这个整体步骤都是在启动的时候添加的, 后面的 getConnect 方法都只是获取了。 

最后再放这张图简单总结下。

MyBatis Plus 插件 动态数据源实现原理与源码讲解 (dynamic-datasource-spring-boot-starter-master),第33张