问题
-
为什么要实现数据源动态切换?
首先我们的项目是集成MyBatis或MyBatis-Plus的。当我们每次执行sql操作方法时,MyBatis或MP会从数据源里拿到一个数据库连接和数据库通信并执行sql。当多个数据源时要保证sql能到对应的数据库上,要求我们做到每一个不同数据源的sql都要拿到对应的数据源并获取相应数据库连接。
-
dynamic-datasource怎么实现数据源动态切换?
-
多数据源集合: 既然需要从不同数据源拿数据库连接,那怎么获取数据源呢?只要你拿着数据源名称来,我根据名称提供给你数据源就好了,所以我们可以在路由类里维护一个Map集合,key为数据源名称,value为数据源。 -
注解+切面:通过自定义注解加切面,就可以拿到每个sql操作方法的所需数据源的名称,然后再去多数据源集合里取出对应的数据源。
-
源码分析
DynamicRoutingDataSource就是dd的自定义连接池类。AbstractRoutingDataSource就是它的父类且为抽象类。
多数据源集合源码分析
1、如何从多数据源集合中获取数据源?
-
AbstractRoutingDataSource#getConnection
当从数据源拿数据库连接,就是调用DataSource接口的getConnection方法,dd实现了此方法。
/**
* 抽象获取连接池
*
* @return 连接池
*/
protected abstract DataSource determineDataSource();
@Override
public Connection getConnection() throws SQLException {
// 多数据源事务id
String xid = TransactionContext.getXID();
if (StringUtils.isEmpty(xid)) {
// 无事务走这一步,这里暂且不看事务代码,determineDataSource是抽象方法由子类实现
return determineDataSource().getConnection();
} else {
// 多数据源事务相关
String ds = DynamicDataSourceContextHolder.peek();
ds = StringUtils.isEmpty(ds) ? "default" : ds;
ConnectionProxy connection = ConnectionFactory.getConnection(ds);
return connection == null ? getConnectionProxy(ds, determineDataSource().getConnection()) : connection;
}
}
-
DynamicRoutingDataSource#determineDataSource
public DataSource determineDataSource() {
// 得到数据源名称:DynamicDataSourceContextHolder是ThreadLocal里面装了一个栈容器,
// 栈里装着当前线程里sql操作方法的数据源名称
String dsKey = DynamicDataSourceContextHolder.peek();
// 获取数据源
return getDataSource(dsKey);
}
-
DynamicRoutingDataSource#getDataSource
获取数据源
public DataSource getDataSource(String ds) {
if (StringUtils.isEmpty(ds)) {
// 没有数据源名称,获取默认数据源
return determinePrimaryDataSource();
} else if (!groupDataSources.isEmpty() && groupDataSources.containsKey(ds)) {
log.debug("dynamic-datasource switch to the datasource named [{}]", ds);
// 从分组数据源中获取一个数据源:例如有三台从节点数据库名为slave_1,
// slave_2, slave_3,dd会根据下划线划分若前缀相同则为同一组数据源
// 这里的三个节点就为slave这一组的分组数据源,组内默认是按负载均衡提供数据源
return groupDataSources.get(ds).determineDataSource();
} else if (dataSourceMap.containsKey(ds)) {
log.debug("dynamic-datasource switch to the datasource named [{}]", ds);
// 没有分组数据源,正常就走一步,直接拿
// 这个dataSourceMap是key为数据源名称,value为数据源的集合,
// 它包含了所有的数据源
return dataSourceMap.get(ds);
}
if (strict) {
throw new CannotFindDataSourceException("dynamic-datasource could not find a datasource named" + ds);
}
// 非严格模式,获取默认数据源
return determinePrimaryDataSource();
}
2、如何装配数据源到多数据源集合中?
-
DynamicRoutingDataSource#afterPropertiesSet
DynamicRoutingDataSource实现了InitializingBean接口,当DynamicRoutingDataSource的Bean在属性注入完后,处于初始化阶段时会调用此接口的afterPropertiesSet方法,下图为Spring Bean生命周期的扩展点及调用顺序。
public void afterPropertiesSet() throws Exception {
// 检查开启了配置但没有相关依赖
checkEnv();
// 添加并分组数据源
Map<String, DataSource> dataSources = new HashMap<>(16);
// 把配置文件中的数据源配置解析为数据源类,并放入Map中
// key为数据源名称,value为数据源类
for (DynamicDataSourceProvider provider : providers) {
dataSources.putAll(provider.loadDataSources());
}
// 把数据源放到全局数据源Map和全局分组数据源Map中
for (Map.Entry<String, DataSource> dsItem : dataSources.entrySet()) {
addDataSource(dsItem.getKey(), dsItem.getValue());
}
// 检测默认数据源是否设置
if (groupDataSources.containsKey(primary)) {
log.info("dynamic-datasource initial loaded [{}] datasource,primary group datasource named [{}]", dataSources.size(), primary);
} else if (dataSourceMap.containsKey(primary)) {
log.info("dynamic-datasource initial loaded [{}] datasource,primary datasource named [{}]", dataSources.size(), primary);
} else {
log.warn("dynamic-datasource initial loaded [{}] datasource,Please add your primary datasource or check your configuration", dataSources.size());
}
}
注解+切面源码分析
-
DynamicDataSourceAnnotationInterceptor#invoke()、determineDatasourceKey()
该类实现了AOP的拦截器接口,在执行sql操作方法时进行拦截并增强该方法。
@override
public Object invoke(MethodInvocation invocation) throws Throwable {
// try{}里面执行的就是原始的sql操作方法,这里是在原方法前
// 找到该方法上是否有自定义注解,自定义注解里面的值即为数据源名称
String dsKey = determineDatasourceKey(invocation);
// 将数据源名称放入当前线程容器中
DynamicDataSourceContextHolder.push(dsKey);
try {
// 在原sql操作方法执行时,MyBatis或MP会根据从上文提到的
// 多数据源集合中通过当前线程容器中的数据源名称拿到数据源,
// 从而获取正确的数据库连接,执行后续过程
return invocation.proceed();
} finally {
// 完成sql操作后,清除线程容器中的数据源名称
DynamicDataSourceContextHolder.poll();
}
}
// 用于找到方法上或类上的自定义注解的属性值
private String determineDatasourceKey(MethodInvocation invocation) {
String key = dataSourceClassResolver.findKey(invocation.getMethod(), invocation.getThis());
return key.startsWith(DYNAMIC_PREFIX) ? dsProcessor.determineDatasource(invocation, key) : key;
}
DS
动态数据源路由注解,属性值即为数据源名称
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DS {
/**
* groupName or specific database name or spring SPEL name.
*
* @return the database you want to switch
*/
String value();
}