MybatisPlus多数据源原理及使用注意点

MybatisPlus多数据源原理及使用注意点

本文介绍的是代码原理以及关联出现的使用注意点,以3.3.1版本为例

基本的配置使用方式可以看这篇文章: https://blog.csdn.net/Azhuzhu_chaste/article/details/113741991

官方文档(有毒,部分收费):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}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Mybatis Plus多数据源原理是通过动态切换数据源实现的。在Mybatis Plus中,可以配置多个数据源,并使用一个数据源上下文来管理当前使用的数据源。具体原理如下: 1. 配置多个数据源:在Spring Boot的配置文件中,配置多个数据源的连接信息,包括数据库地址、用户名、密码等。 2. 创建数据源对象:根据配置的数据源信息,创建多个数据源对象,例如使用`DruidDataSource`来创建连接池。 3. 创建SqlSessionFactory:针对每个数据源,分别创建对应的`SqlSessionFactory`,用于生成`SqlSession`。 4. 创建数据源上下文:通过自定义的数据源上下文对象,管理当前使用的数据源。可以使用`ThreadLocal`来保存当前线程使用的数据源。 5. 动态切换数据源:在需要切换数据源的地方,通过调用数据源上下文的切换方法,设置当前线程使用的数据源。这样,在执行数据库操作时,Mybatis Plus会根据当前线程使用的数据源来选择对应的`SqlSessionFactory`和`SqlSession`。 6. 数据库操作:通过Mybatis Plus提供的API进行数据库操作,例如执行查询、插入、更新等操作。Mybatis Plus会根据当前线程使用的数据源来选择对应的`SqlSessionFactory`和`SqlSession`。 通过上述步骤,就可以实现在同一个项目中使用多个数据源进行数据库操作。需要注意的是,配置多数据源时要确保数据源的唯一标识不重复,并且在切换数据源时要注意线程安全。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

weixin_42057187

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值