MybatisPlus多数据源原理及使用注意点
本文介绍的是代码原理以及关联出现的使用注意点,以3.3.1版本为例
基本的配置使用方式可以看这篇文章:
官方文档(有毒,部分收费):https://www.kancloud.cn/tracy5546/dynamic-datasource/2264611
数据源加载流程
1. 自动配置加载所有数据源信息
在配置配置类的DynamicDataSourceAutoConfiguration
,自动读取我们配置的数据源信息到DynamicDataSourceProvider
里
public class DynamicDataSourceAutoConfiguration {
private final DynamicDataSourceProperties properties;
@Bean
@ConditionalOnMissingBean
public DynamicDataSourceProvider dynamicDataSourceProvider() {
Map<String, DataSourceProperty> datasourceMap = properties.getDatasource();
return new YmlDynamicDataSourceProvider(datasourceMap);
}
...
}
2. 根据配置信息构造数据源
由DynamicDataSourceProvider
提供数据源的构造方法 createDataSourceMap
public abstract class AbstractDataSourceProvider implements DynamicDataSourceProvider {
@Autowired
private DefaultDataSourceCreator defaultDataSourceCreator;
protected Map<String, DataSource> createDataSourceMap(
Map<String, DataSourceProperty> dataSourcePropertiesMap) {
Map<String, DataSource> dataSourceMap = new HashMap<>(dataSourcePropertiesMap.size() * 2);
for (Map.Entry<String, DataSourceProperty> item : dataSourcePropertiesMap.entrySet()) {
DataSourceProperty dataSourceProperty = item.getValue();
String poolName = dataSourceProperty.getPoolName();
if (poolName == null || "".equals(poolName)) {
poolName = item.getKey();
}
dataSourceProperty.setPoolName(poolName);
dataSourceMap.put(poolName, defaultDataSourceCreator.createDataSource(dataSourceProperty));
}
return dataSourceMap;
}
}
3. 数据源加载
在核心动态数据源组件DynamicRoutingDataSource
,通过 InitializingBean
,在bean加载完毕之后进行数据源的加载。
这一步由afterPropertiesSet()
触发数据源的加载,并将数据源与对应的名称存入一个 ConcurrentHashMap
中,供后面数据源获取以及切换使用。
@Slf4j
public class DynamicRoutingDataSource extends AbstractRoutingDataSource implements InitializingBean, DisposableBean {
private static final String UNDERLINE = "_";
/**
* 所有数据库
*/
private final Map<String, DataSource> dataSourceMap = new ConcurrentHashMap<>();
/**
* 分组数据库
*/
private final Map<String, GroupDataSource> groupDataSources = new ConcurrentHashMap<>();
...
/**
* 添加数据源
*
* @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) {
try {
closeDataSource(oldDataSource);
} catch (Exception e) {
log.error("dynamic-datasource - remove the database named [{}] failed", ds, e);
}
}
log.info("dynamic-datasource - load a datasource named [{}] success", ds);
}
/**
* 新数据源添加到分组
*
* @param ds 新数据源的名字
* @param dataSource 新数据源
*/
private void addGroupDataSource(String ds, DataSource dataSource) {
if (ds.contains(UNDERLINE)) {
String group = ds.split(UNDERLINE)[0];
GroupDataSource groupDataSource = groupDataSources.get(group);
if (groupDataSource == null) {
try {
groupDataSource = new GroupDataSource(group, strategy.getDeclaredConstructor().newInstance());
groupDataSources.put(group, groupDataSource);
} catch (Exception e) {
throw new RuntimeException("dynamic-datasource - add the datasource named " + ds + " error", e);
}
}
groupDataSource.addDatasource(ds, dataSource);
}
}
...
@Override
public void afterPropertiesSet() throws Exception {
// 检查开启了配置但没有相关依赖
checkEnv();
// 添加并分组数据源
Map<String, DataSource> dataSources = provider.loadDataSources();
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 {
throw new RuntimeException("dynamic-datasource Please check the setting of primary");
}
}
...
}
数据源切换原理
切换数据源工具类
DynamicDataSourceContextHolder
使用ThreadLocal
记录了当前数据源切换的队列,用于支持事务的嵌套切换。当前数据源以及数据源的切换是基于该工具类的。
/**
* 核心基于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 数据源名称
*/
public static void push(String ds) {
LOOKUP_KEY_HOLDER.get().push(StringUtils.isEmpty(ds) ? "" : ds);
}
/**
* 清空当前线程数据源
* <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();
}
}
多数据源切换切面处理
DynamicDataSourceAnnotationInterceptor
作为 @DS
的处理类, 提供了指定数据源切换以及动态切换的功能
public class DynamicDataSourceAnnotationInterceptor implements MethodInterceptor {
/**
* The identification of SPEL.
*/
private static final String DYNAMIC_PREFIX = "#";
private final DataSourceClassResolver dataSourceClassResolver;
private final DsProcessor dsProcessor;
public DynamicDataSourceAnnotationInterceptor(Boolean allowedPublicOnly, DsProcessor dsProcessor) {
dataSourceClassResolver = new DataSourceClassResolver(allowedPublicOnly);
this.dsProcessor = dsProcessor;
}
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
String dsKey = determineDatasourceKey(invocation);
DynamicDataSourceContextHolder.push(dsKey);
try {
return invocation.proceed();
} finally {
DynamicDataSourceContextHolder.poll();
}
}
/**
* 提供动态选择数据源的方法
* 进行key的判断,如果是#开头,会去进行动态判断,返回对应的数据源
* 目前内置三种处理器,可以通过继承DsProcessor进行拓展:
* DsHeaderProcessor 请求头匹配
* DsSessionProcessor session匹配
* DsSpelExpressionProcessor 请求参数匹配
*
*/
private String determineDatasourceKey(MethodInvocation invocation) {
String key = dataSourceClassResolver.findDSKey(invocation.getMethod(), invocation.getThis());
return (!key.isEmpty() && key.startsWith(DYNAMIC_PREFIX)) ? dsProcessor.determineDatasource(invocation, key) : key;
}
}
数据源获取
这里获取数据源的同时,取到了数据库连接,用于执行SQL。其中有指定数据源或分组数据数据源的判断。
public class DynamicRoutingDataSource extends AbstractRoutingDataSource implements InitializingBean, DisposableBean {
...
@Override
public DataSource determineDataSource() {
return getDataSource(DynamicDataSourceContextHolder.peek());
}
private DataSource determinePrimaryDataSource() {
log.debug("dynamic-datasource switch to the primary datasource");
return groupDataSources.containsKey(primary) ? groupDataSources.get(primary).determineDataSource() : dataSourceMap.get(primary);
}
/**
* 获取当前所有的数据源
*
* @return 当前所有数据源
*/
public Map<String, DataSource> getCurrentDataSources() {
return dataSourceMap;
}
/**
* 获取的当前所有的分组数据源
*
* @return 当前所有的分组数据源
*/
public Map<String, GroupDataSource> getCurrentGroupDataSources() {
return groupDataSources;
}
/**
* 获取数据源
*
* @param ds 数据源名称
* @return 数据源
*/
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);
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 RuntimeException("dynamic-datasource could not find a datasource named" + ds);
}
return determinePrimaryDataSource();
}
...
}
功能使用注意点
使用方法
通过MybatisPlus切换数据源有两种方式:
- 使用
@DS
,通过AOP进行切换,自己注意下AOP的内部调用生效问题就好; - 使用
DynamicDataSourceContextHolder
提供的方法进行切换,不推荐使用,如果为了适应一些复杂场景不得不使用时,记得在finally
调用poll()
或者clear()
进行数据源清理。
功能特性
从以上逻辑可以看出,MybaitsPlus的多数据源功能有以下拓展功能。
嵌套切换
- 支持嵌套使用,方法结束后会切换回上个数据源,不用担心数据源切换错乱的问题;
- 方法抛出异常后,方法会通过AOP层层退出,到达类似Controller的全局异常拦截时,数据源已经切换回默认数据源;
数据源分组
多数据源支持分组功能:
同一分组的数据源会以负载均衡的方式轮流访问,通过数据源命名进行配置,命名规则为:[数据源名]_[数据源标识]。
例如同时定义了slave1_node1与slave1_node2,这两个就会划为一组,通过 @DS("slave1")
来负载均衡使用,同时也可以@DS("slave1_node2")
来切换指定的数据源
动态选择数据源
框架提供了数据源的动态选择方式,可以通过继承DsProcessor
进行拓展。
当@DS
的参数以 #
开头时,便可进行选择器。DsSpelExpressionProcessor
为默认的筛选器,匹配优先级最低,但是会做为保底匹配
框架自带了三种选择器
实现类 | 功能 | 参数格式 |
DsHeaderProcessor | 根据请求头匹配 | #header + 【任意单个字符作为分隔】+ 【请求头参数名】 |
DsSessionProcessor | 根据session匹配 | #session + 【任意单个字符作为分隔】+ 【session参数名】 |
DsSpelExpressionProcessor | 根据请求参数匹配 | 默认选择器,对于默认不设置的情况下,从参数中取值的方式 #param1 设置指定模板 ParserContext.TEMPLATE_EXPRESSION 后的取值方式: #{#param1} issues: https://github.com/baomidou/dynamic-datasource-spring-boot-starter/issues/199 |