目录
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 那一步获取到的数据源,直接返回该数据源
基本原理,可看这个简易图
3. 源码介绍
源码地址 https://gitee.***/baomidou/dynamic-datasource-spring-boot-starter
(源码一定要自己完整看一遍,此篇博客只展示部分关键源码)
3.1 使用 AOP 拦截,方法执行前获取到当前方法要用的数据源
@DS 注解代表定义当前方法、当前类使用哪个数据源
value 指定当前类、方法使用的数据源名称
数据源名称也是在配置文件中定义的
注解处理切面 DynamicDataSourceAnnotationAdvisor
切面 advice 由外部传过来,要处理的注解也从外面传过来。
也就是这里,这行代码的意思是
DynamicDataSourceAnnotationInterceptor 负责处理 DS 注解
接着看 DynamicDataSourceAnnotationInterceptor 如何处理
下面分别解释下
1. 将 @DS 注解的 value 值压入 ThreadLocal 当前线程的栈
看这段方法 DynamicDataSourceContextHolder.push(dsKey);
ThreadLocal 中存储的是 Deque 类,也就是一个双端队列(两头都可以插入的队列) ,使用的是 ArrayDeque 双端队列,内部是一个数组。
为什么使用队列,而不是简单一个字符串,注释已经写的很清楚了,看注释即可。
ArrayDeque 的 push 就是在队列首部添加一个元素。
2. 调用实际的方法
这里不是切面吗,实际方法也就是被拦截的方法。也就是直接调用业务逻辑。
3. 将第 1 步中的元素弹出来
业务逻辑执行完成后,就将刚才加入的元素弹出来。
其实这里很像 JVM 虚拟机栈,方法调用就是压入栈,方法结束调用就是出栈,栈顶就是当前执行的方法。
而此处这里的栈顶就代表 当前正在执行的方法所用的 数据源名称。而方法执行完了,这里也该出栈了。
这里核心逻辑已经完了,本质就是这么简单。
3.2 实现自定义 DataSource 接口,实现 DataSource 接口的 getConnect 方法做动态处理
DynamicRoutingDataSource 代表动态路由数据源
做的事情就是运行时动态路由出一个当前需要的数据源。
接着看源码,首先与 SpringBoot 整合时 自动配置出当前 Bean
DynamicRoutingDataSource 类图
3.2.1 DataSource
代表一个数据源,由javax 扩展定义
3.2.2 AbstractDataSource
抽象实现,将一些对数据源的配置操作都实现为不支持操作抛出异常 UnsupportedOperationException
(动态数据源相当于一个代理,不需要给动态数据源本身设置相关配置)
3.2.3 AbstractRoutingDataSource
抽象实现,路由动态配置源,实现了关键方法 getConnection ,完成了路由操作
看看源码 getConnection()
getConnection() 何时调用呢,也就是上一步的切面中的第 2 步中,invocation.proceed(),执行业务逻辑的过程中,遇到的 数据库层的操作时,就会到这里了。
这里直接看简单的非事务的获取数据源这里。
关键代码 determineDataSource().getConnection()
这个方法由子类实现,也就是下面的 DynamicRoutingDataSource 类
3.2.4 DynamicRoutingDataSource
动态路由数据源核心实现,完成 数据源的维护(添加删除数据源)、数据源的选择
接着上面的源码流程,子类的 determineDataSource 方法最终调用了 getDataSource
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
2. 实现自定义 DataSource 接口,实现 DataSource 接口的 getConnect 方法做动态处理
从分组找一个数据源 groupDataSources.get(ds).determineDataSource();
分组是什么意思?
分组定义的规则是 group_xxx,也就是数据源名称以下划线分割,下划线前面的就是组名。
分组的作用是什么呢?本质用于实现一个名称对应多数据库源。
比如一主多从,可以将从数据源都分到 slave 组里面,用的时候就是 @DS("slave") // 组名
在实际决定数据源的时候,就会按照一定的策略从这个组里的数据源挑选一个了。
接着看源码,如何 从分组数据源中找一个数据源
groupDataSources.get(ds).determineDataSource();
最后到了策略的选择,DynamicDataSourceStrategy
DynamicDataSourceStrategy 有两个实现类
LoadBalanceDynamicDataSourceStrategy 负载均衡动态数据源策略
看源码,这个就是按顺序一个个选择下来,达到负载均衡方式
RandomDynamicDataSourceStrategy 随机动态数据源策略
这个完全就是纯随机选一个
3. 直接根据名称找一个数据源
如果走到了这里,说明这个数据源名称没有配置分组,那就直接根据名称取这单个数据源了
直接纯 get 了
数据源何时初始化的
还是在 DynamicRoutingDataSource,这个类实现了 Spring InitializingBean
接口回调方法 afterPropertiesSet,当当前 Bean 内部的属性都初始化完毕了后就回调这个方法
看看 afterPropertiesSet 回调方法内容
这里只看关键代码
1. dataSources.putAll(provider.loadDataSources());
@Autowired private List<DynamicDataSourceProvider> providers; providers 是什么呢 ?
providers 代表 动态数据源配置的来源,默认实现就是从 yml 中来,也就是 SpringBoot 的 application.yml 配置
默认实现
传进去的参数配置类
DynamicDataSourceProvider 也就是解析了这些配置 来获取到所有配置
拿到配置后,就要解析这些配置了 ,这里委托了父类处理
这里完成创建数据源,然后将结果封装成了 Map<String, DataSource> dataSourceMap 返回
(泛型为 <数据源名称,数据源实例>)
看看如何创建数据源的 defaultDataSourceCreator.createDataSource(dataSourceProperty)
大致流程如下:
这里介绍一下 creators
dynamic-datasource-creator 模块下定义了单独数据源创建的代码
DataSourceCreator 代表一个数据源创建器,用于创建一个数据源。
每种数据源类型都有自己的创建器,比如这里常见的 Druid、Hikar
这里就举例其中一个 HikariDataSourceCreator,其他的都差不多
HikariDataSourceCreator
调用这些创造器的创建的时候默认直接就启动了,除非配置了懒加载。
到现在,数据源就已经创建完了。再次说一下这是在Spring 的 afterPropertiesSet 回调里完成创建的。(afterPropertiesSet 即当前 Bean 的所有属性 Spring 都填充完毕后回调)
2. addDataSource(dsItem.getKey(), dsItem.getValue());
上一步的 provider.loadDataSources() 讲解完毕了,这次看看下面的 addDataSource(dsItem.getKey(), dsItem.getValue());
addDataSource 方法
首先是先给 dataSourceMap 放进去了。这里会返回旧的数据源(如果是第一次加入,则返回null),所以下面判断了如果返回有值旧关闭掉旧的数据源,关闭就是调用数据源的 close 方法。
然后是 addGroupDataSource
这里数据源就完成了添加,这个整体步骤都是在启动的时候添加的, 后面的 getConnect 方法都只是获取了。
最后再放这张图简单总结下。