目录
DynamicDataSourceAutoConfiguration.class自动装配类
DynamicRoutingDataSource数据源主要方法
DynamicDataSourceContextHolder线程上下文
@DS切面DynamicDataSourceAnnotationAdvisor
DynamicDataSourceAnnotationInterceptor拦截器
1、前置准备
依赖引入
XML
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
测试数据库准备

2、多数据源查询切换
数据库配置文件
创建两个测试数据库来进行模拟
java
spring:
datasource:
dynamic:
primary: db1
strict: false
datasource:
db1:
url: jdbc:mysql://localhost:3306/test-db1?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
db2:
url: jdbc:mysql://localhost:3306/test-db2?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
参数说明
所有的数据库配置都需要放在dynamic下,这样才能读取到
primary:默认使用哪个库,也就是说当我们没有指定使用哪个数据库时,默认使用的就是这个

strict: 这个参数如果选择true,当我们指定错误的数据库名,就会直接报错。如果时false,那就使用默认的数据库

datasource
这个下面放列表,放的是所有数据库的连接信息
db1、db2可以理解为数据库的别名,后面根据这个名来指定数据库
数据源切换:@DS注解
注解内接收一个参数:这个参数是用来指定使用哪个数据源的(必填)


结果
我们可以看到项目中已经实现了数据源切换

日志
我们可以通过日志查看使用了哪个库
在配置文件中添加:
java
server:
port: 8080
logging:
level:
com.baomidou.dynamic.datasource: debug
这样我们就可以看到每次使用了哪个库

3、多数据源插入切换
有时候针对一个表单提交要插入两个库,就可以来切换数据源来实现插入数据到不同的库
示例代码


测试结果
调用接口,两条数据插入到不同的库里

错误示范(同一类中调用)
此为错误示范:因为@DS注解实际还是通过AOP操作切换数据源的,所以同类中调用会失效


当我们调用时可以看到没有报错,看下日志:

这个日志是没有指定数据源使用默认数据源的提示,就是因为同类调用导致失效了。

我们也可以发现数据都插入到db1库中了
4、多数据源事务
多数据源事务,我们不再使用@Transactional而是使用@DSTransactional
注意(重要)
@DSTransactional是弱一致性,因为两个数据源的提交是分为两次的,如果发生网络异常、宕机,可能就会导致回滚失败,因为提交多次非原子性
5、源码讲解
在学习源码前我们可以理解下mybatis是怎么发起连接数据库的,是通过注入一个DataSource对象,通过这个对象的getConnection对象来获取数据库连接的
下面开始讲解dynamic多数据源实现源码
1、数据源读取
在我们引入dynamic-datasource-spring-boot-starter依赖时,会引入一个自动装配类
DynamicDataSourceAutoConfiguration.class自动装配类
这个类会读取配置中的所有数据源,封装成一个Map对象,map的key存储的是我们放的数据源名,value就是一个个的DataSource数据源对象(DataSource中存的就是数据库的连接信息)

从这里可以看到读取的就是spring.datasource.dynamic下的配置信息
java
@Bean
@Order(0)
public DynamicDataSourceProvider ymlDynamicDataSourceProvider() {
return new YmlDynamicDataSourceProvider(this.properties.getDatasource());
}
这个Bean的作用就是加载配置文件中的所有配置信息返回一个DynamicDataSourceProvider对象,入参是:
this.properties.getDatasource()
这个是获取所有的数据源配置信息

为什么要返回DynamicDataSourceProvider对象,因为在构建数据源对象DynamicRoutingDataSource要用,也就是重点
这个bean是一个重点!!前面说的mybatis是通过datasource来获取连接的,这里就返回了一个datasource对象,我们可以具体看下这个bean都做了什么
java
@Bean
@ConditionalOnMissingBean
public DataSource dataSource() {
DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource();
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());
return dataSource;
}

DynamicRoutingDataSource数据源主要方法
这个DynamicRoutingDataSource对象有什么用呢?这个其实也是一个DataSource的实现子类,只不过现在它可以理解为一个路由。
这个对象有方法加载了所有的数据源信息,并且有方法可以根据数据库名返回对应的数据源
这个对象中存了项目中定义的数据源信息:
private final Map<String, DataSource> dataSourceMap = new ConcurrentHashMap();
具体方法:
afterPropertiesSet()
java
public void afterPropertiesSet() throws Exception {
this.checkEnv();
Map<String, DataSource> dataSources = new HashMap(16);
Iterator var2 = this.providers.iterator();
while(var2.hasNext()) {
DynamicDataSourceProvider provider = (DynamicDataSourceProvider)var2.next();
dataSources.putAll(provider.loadDataSources());
}
var2 = dataSources.entrySet().iterator();
while(var2.hasNext()) {
Map.Entry<String, DataSource> dsItem = (Map.Entry)var2.next();
this.addDataSource((String)dsItem.getKey(), (DataSource)dsItem.getValue());
}
if (this.groupDataSources.containsKey(this.primary)) {
log.info("dynamic-datasource initial loaded [{}] datasource,primary group datasource named [{}]", dataSources.size(), this.primary);
} else if (this.dataSourceMap.containsKey(this.primary)) {
log.info("dynamic-datasource initial loaded [{}] datasource,primary datasource named [{}]", dataSources.size(), this.primary);
} else {
log.warn("dynamic-datasource initial loaded [{}] datasource,Please add your primary datasource or check your configuration", dataSources.size());
}
}
这个方法是bean生命周期中的初始化时的操作,加载所有的数据源到Map<String, DataSource> dataSourceMap对象中,通过前面的DynamicDataSourceProvider对象来获取
addDataSource(String ds, DataSource dataSource)
添加数据源到Map中,上面方法有调用
java
public synchronized void addDataSource(String ds, DataSource dataSource) {
DataSource oldDataSource = (DataSource)this.dataSourceMap.put(ds, dataSource);
this.addGroupDataSource(ds, dataSource);
if (oldDataSource != null) {
this.closeDataSource(ds, oldDataSource);
}
log.info("dynamic-datasource - add a datasource named [{}] success", ds);
}
getDataSource(String ds)
这个方法是重点,这个方法实际就是返回一个DataSource供mybatis使用的,正如前面说的mybatis会调用当前项目中的DataSource对象的getConnect方法来获取数据库连接,而我们这个方法正是返回了一个DataSource。
这个方法的入参是ds,也就是我们的数据源名称,通过数据源名称从所有数据源的map中获取对象的DataSource对象
可以看到里面打印的日志log.debug也正是我们前面测试打印的
java
public DataSource getDataSource(String ds) {
if (StringUtils.isEmpty(ds)) {
return this.determinePrimaryDataSource();
} else if (!this.groupDataSources.isEmpty() && this.groupDataSources.containsKey(ds)) {
log.debug("dynamic-datasource switch to the datasource named [{}]", ds);
return ((GroupDataSource)this.groupDataSources.get(ds)).determineDataSource();
} else if (this.dataSourceMap.containsKey(ds)) {
log.debug("dynamic-datasource switch to the datasource named [{}]", ds);
return (DataSource)this.dataSourceMap.get(ds);
} else if (this.strict) {
throw new CannotFindDataSourceException("dynamic-datasource could not find a datasource named" + ds);
} else {
return this.determinePrimaryDataSource();
}
}
DynamicDataSourceContextHolder线程上下文
这个上下文就是来定义什么时候使用哪个数据源
java
private static final ThreadLocal<Deque<String>> LOOKUP_KEY_HOLDER = new NamedThreadLocal<Deque<String>>("dynamic-datasource") {
protected Deque<String> initialValue() {
return new ArrayDeque();
}
};
主要有以下方法:
String push(String ds)将当前线程使用的数据源的名称存入线程中
java
public static String push(String ds) {
String dataSourceStr = StringUtils.isEmpty(ds) ? "" : ds;
((Deque)LOOKUP_KEY_HOLDER.get()).push(dataSourceStr);
return dataSourceStr;
}
String peek()获取当前线程中使用的数据源名称
java
public static String peek() {
return (String)((Deque)LOOKUP_KEY_HOLDER.get()).peek();
}
java
public static void clear() {
LOOKUP_KEY_HOLDER.remove();
}
java
public static void poll() {
Deque<String> deque = (Deque)LOOKUP_KEY_HOLDER.get();
deque.poll();
if (deque.isEmpty()) {
LOOKUP_KEY_HOLDER.remove();
}
}
@DS切面DynamicDataSourceAnnotationAdvisor
定义要拦截带有@DS注解的类或者方法,交给DynamicDataSourceAnnotationInterceptor拦截器处理
DynamicDataSourceAnnotationInterceptor拦截器
这里是真正操作实现切换数据源的地方
determineDatasourceKey(MethodInvocation invocation)
这个方法是获取注解上的数据源的名称,也就是@DS(value)中value值
java
private String determineDatasourceKey(MethodInvocation invocation) {
String key = this.dataSourceClassResolver.findKey(invocation.getMethod(), invocation.getThis());
return key.startsWith("#") ? this.dsProcessor.determineDatasource(invocation, key) : key;
}
invoke(MethodInvocation invocation)
这个方法是将当前要是使用的线程上下文设置到数据源的名称存入线程中和使用完之后清理
java
public Object invoke(MethodInvocation invocation) throws Throwable {
String dsKey = this.determineDatasourceKey(invocation);
DynamicDataSourceContextHolder.push(dsKey);
Object var3;
try {
var3 = invocation.proceed();
} finally {
DynamicDataSourceContextHolder.poll();
}
return var3;
}
AbstractRoutingDataSource类
这个类是上面的DynamicRoutingDataSource的父类,也正如我们前面说的mybatis获取数据库连接是通过DataSource方法的getConnect方法来获取的,这个方法中就定义了
getConnection()
java
public Connection getConnection() throws SQLException {
String xid = TransactionContext.getXID();
if (StringUtils.isEmpty(xid)) {
return this.determineDataSource().getConnection();
} else {
String ds = DynamicDataSourceContextHolder.peek();
ds = StringUtils.isEmpty(ds) ? this.getPrimary() : ds;
ConnectionProxy connection = ConnectionFactory.getConnection(ds);
return (Connection)(connection == null ? this.getConnectionProxy(ds, this.determineDataSource().getConnection()) : connection);
}
}
通过获取线程上下文中的使用的数据源,如果数据源为空,就用默认数据源。
this.determineDataSource()获取数据源对象
这个方法是这个AbstractRoutingDataSource类中的抽象方法,子类DynamicRoutingDataSource实现了这个方法
java
public DataSource determineDataSource() {
String dsKey = DynamicDataSourceContextHolder.peek();
return this.getDataSource(dsKey);
}
至此形成调用闭环
源码核心类
DynamicDataSourceAutoConfiguration
DynamicRoutingDataSource
AbstractRoutingDataSource
DynamicDataSourceAnnotationInterceptor
DynamicDataSourceAnnotationAdvisor