动态数据源DynamicDatasource
简介
本文介绍baomidou开源的多数据源扩展插件DynamicDatasource,并用来实现动态数据源及以下功能
介绍功能包括:
- 提供 自定义数据源来源 方案(如全从数据库加载)。
- 提供项目启动后 动态增加移除数据源 方案。
- 支持 自定义注解 ,需继承DS(3.2.0+)。
本文Demo完整源码:
Github源代码地址:https://github.com/xunfeng224/Springboot/tree/main/springboot-DynamicDatasource
Gitee源代码地址:https://gitee.com/xfeng520/Springboot/tree/main/springboot-DynamicDatasource
[!CAUTION]
运行源码需注意,由于加入了手动实现的动态数据源,会导致Bean冲突,若想运行DynamicDataSource,将手动代码dynamic包直接删除,若想运行手动实现的动态数据源,将
LoadDataSourceRunner
类全部注释掉,避免找不到Bean报错。手动动态数据源相关代码全部位于dynamic包下。原因:自定义数据源管理类
DynamicDataSource
继承了Spring的AbstractRoutingDataSource
类,而在DynamicDataSource源码中,DynamicRoutingDataSource
类同样继承了AbstractRoutingDataSource
,本来想通过自定义Bean注入名称来解决Bean冲突,发现不可行。在DynamicDataSourceAutoConfiguration
中注册DynamicRoutingDataSource
的bean时,加入了@ConditionalOnMissingBean
注解,这将导致有自定义实现类bean注入Spring容器时,DynamicRoutingDataSource
无法注入Spring容器,从而启动报错。@ConditionalOnMissingBean作用:判断当前需要注入Spring容器中的bean的实现类是否已经含有,有的话不注入,没有就注入
@Bean @ConditionalOnMissingBean public DataSource dataSource(List<DynamicDataSourceProvider> providers) { DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource(providers); dataSource.setPrimary(this.properties.getPrimary()); dataSource.setStrict(this.properties.getStrict()); dataSource.setStrategy(this.properties.getStrategy()); dataSource.setP6spy(this.properties.getP6spy()); dataSource.setSeata(this.properties.getSeata()); dataSource.setGraceDestroy(this.properties.getGraceDestroy()); return dataSource; }
源码分析
ThreadLocal和AbstractRoutingDataSource
ThreadLocal
:全称:thread local variable
。主要是为解决多线程时由于并发而产生数据不一致问题。ThreadLocal为每个线程提供变量副本,确保每个线程在某一时间访问到的不是同一个对象,这样做到了隔离性,增加了内存,但大大减少了线程同步时的性能消耗,减少了线程并发控制的复杂程度。
- ThreadLocal作用:在一个线程中共享,不同线程间隔离
- ThreadLocal原理:ThreadLocal存入值时,会获取当前线程实例作为key,存入当前线程对象中的Map中。
AbstractRoutingDataSource
:根据用户定义的规则选择当前的数据源,
Spring boot提供了AbstractRoutingDataSource 根据用户定义的规则选择当前的数据源,这样我们可以在执行查询之前,设置使用的数据源。实现可动态路由的数据源,在每次数据库查询操作前执行。它的抽象方法 determineCurrentLookupKey() 决定使用哪个数据源。
DynamicRoutingDataSource
该类继承上述提到的AbstractRoutingDataSource
抽象类,实现determineDataSource()
方法,如上文所述,该方法决定了当前数据库操作所使用的数据源
public class DynamicRoutingDataSource extends AbstractRoutingDataSource implements InitializingBean, DisposableBean {
//...省略...
/**
* 通过各种方式加载的数据源将存储在该Map中,后续动态切换也是从这里获取
*/
private final Map<String, DataSource> dataSourceMap = new ConcurrentHashMap<>();
/**
* 分组数据库
*/
private final Map<String, GroupDataSource> groupDataSources = new ConcurrentHashMap<>();
/**
获取主数据源
*/
@Override
protected String getPrimary() {
return primary;
}
@Override
public DataSource determineDataSource() {
// 数据源key/数据源名称从DynamicDataSourceContextHolder.peek()中获取
String dsKey = DynamicDataSourceContextHolder.peek();
return getDataSource(dsKey);
}
/**
* 获取数据源
*
* @param ds 数据源名称
* @return 数据源
*/
public DataSource getDataSource(String ds) {
if (DsStrUtils.isEmpty(ds)) {
// 这里数据源名称为空,调用方法获取主数据源
return determinePrimaryDataSource();
} else if (!groupDataSources.isEmpty() && groupDataSources.containsKey(ds)) {
log.debug("dynamic-datasource switch to the datasource named [{}]", ds);
return groupDataSources.get(ds).determineDataSource();
} else if (dataSourceMap.containsKey(ds)) {
log.debug("dynamic-datasource switch to the datasource named [{}]", ds);
return dataSourceMap.get(ds);
}
if (strict) {
throw new CannotFindDataSourceException("dynamic-datasource could not find a datasource named " + ds);
}
return determinePrimaryDataSource();
}
/**
* 添加数据源
*
* @param ds 数据源名称
* @param dataSource 数据源
*/
public synchronized void addDataSource(String ds, DataSource dataSource) {
DataSource oldDataSource = dataSourceMap.put(ds, dataSource);
// 新数据源添加到分组
this.addGroupDataSource(ds, dataSource);
// 关闭老的数据源
if (oldDataSource != null) {
closeDataSource(ds, oldDataSource, graceDestroy);
}
log.info("dynamic-datasource - add a datasource named [{}] success", ds);
}
//...省略...
}
DynamicDataSourceContextHolder
类 ,源码自带注解也挺详细的了,不做多解释
import org.springframework.core.NamedThreadLocal;
import java.util.ArrayDeque;
import java.util.Deque;
/**
* 核心基于ThreadLocal的切换数据源工具类
*
* @author TaoYu Kanyuxia
* @since 1.0.0
*/
public final class DynamicDataSourceContextHolder {
/**
* 为什么要用链表存储(准确的是栈)
* <pre>
* 为了支持嵌套切换,如ABC三个service都是不同的数据源
* 其中A的某个业务要调B的方法,B的方法需要调用C的方法。一级一级调用切换,形成了链。
* 传统的只设置当前线程的方式不能满足此业务需求,必须使用栈,后进先出。
* </pre>
*/
private static final ThreadLocal<Deque<String>> LOOKUP_KEY_HOLDER = new NamedThreadLocal<Deque<String>>("dynamic-datasource") {
@Override
protected Deque<String> initialValue() {
return new ArrayDeque<>();
}
};
private DynamicDataSourceContextHolder() {
}
/**
* 获得当前线程数据源
*
* @return 数据源名称
*/
public static String peek() {
return LOOKUP_KEY_HOLDER.get().peek();
}
/**
* 设置当前线程数据源
* <p>
* 如非必要不要手动调用,调用后确保最终清除
* </p>
*
* @param ds 数据源名称
* @return 数据源名称
*/
public static String push(String ds) {
String dataSourceStr = DsStrUtils.isEmpty(ds) ? "" : ds;
LOOKUP_KEY_HOLDER.get().push(dataSourceStr);
return dataSourceStr;
}
/**
* 清空当前线程数据源
* <p>
* 如果当前线程是连续切换数据源 只会移除掉当前线程的数据源名称
* </p>
*/
public static void poll() {
Deque<String> deque = LOOKUP_KEY_HOLDER.get();
deque.poll();
if (deque.isEmpty()) {
LOOKUP_KEY_HOLDER.remove();
}
}
/**
* 强制清空本地线程
* <p>
* 防止内存泄漏,如手动调用了push可调用此方法确保清除
* </p>
*/
public static void clear() {
LOOKUP_KEY_HOLDER.remove();
}
}
DynamicDatasource快速开始
其实也没啥好写的,DynamicDatasource功能很丰富,但本文章只涉及到简单的操作。流程为新建Springboot项目,引入Maven依赖,配置yml中的master数据源,使用mybatis-plus快速实现查询主数据源sys_user表数据,通过defaultDataSourceCreator.createDataSource(dataSourceProperty)创建数据源,通过dynamicRoutingDataSource.addDataSource(ds.getId().toString(), dataSource);添加数据源,通过DynamicDataSourceContextHolder.push(dsId.toString());切换数据源或通过注解@DS(“master”)切换
项目结构
Maven依赖
引入DynamicDatasource依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>4.3.1</version>
</dependency>
引入其他依赖,为本案例项目所需依赖,非实现动态数据源所必须
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
<dependency