springboot动态数据源原理分析及实现

springboot动态数据源实现的两个关键要素

说起动态数据源,大家肯定都觉得是个很神秘的东西,其实若仔细研究就会发现并不复杂,具体来说,只有两步:
第一、spring容器中注入两个数据源。
第二、在运行时根据业务需要获取特定的数据源。

问:老哥你这不是废话吗,看完你这个讲解,发现它就是讲解,屁用没有。
答:别急麻,现实中咱们解决问题就是,从外到里,一层一层递进。咱们就从第一步spring容器注入多数据源开始递进。
首先咱们要搞明白的,如何注入数据源及注入的数据源由谁来管理呢?
注入数据源我就不讲解了就是声明几个bean,关键是这个多个数据源由谁管理,具体的是哪个类来管理?

讲这个之前,咱们梳理一下平常使用spring+mybatis的操作数据库的流程,基本上就是如下四步。

datasource–>sqlSessionfactory–>connnection–>crud。

大家仔细考虑,sqlsessionFactory会关心你用什么数据源吗?显然它不会,只要你给一个它能用的数据源,他就能帮你干事。所以咱们要实现动态数据源就得在dataSource这里做文章
那怎么做呢?
试想一下,如果咱们有一个自定义的包装类,它继承了DataSource,并且咱们在这个包装类内部维护一个存储多个数据源的MAP,当调用的时候咱们根据业务逻辑传过来的key,动态获取对应的dataSource不就实现这个功能了吗。你看动态数据源它本质就是这么个原理。

同时spring这个框架老哥其实已经帮我们实现了这个包装类,org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource
AbstractRoutingDataSource是一个抽象类,该类中determineTargetDataSource()方法将调用determineCurrentLookupKey()方法来动态获取数据源
我们要做的就是写一个子类继承该类,然后重写里面的determineCurrentLookupKey()方法,用于自定义key的获取方式,一般的我们将数据源的key存放在ThreadLocal中。定义好子类之后,咱们就将声明好的多个数据源配置到该子类中,然后将该类注入到spring容器中,同时交给sqlSessionFactory。
在这里插入图片描述

自定义DynamicDataSource类,包含两块:

1、重写determineCurrentLookupKey()方法,用于从当前线程中获取对应的数据源。
2、CONTEXT_HOLDER 一个ThreaLocal用于数据源与当前线程的绑定与解绑。

package com.app.microservice.config;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import java.util.ArrayDeque;
import java.util.Deque;


/**
 * @author:whh
 * @date: 2022-04-28 21:11
 * <p></p>
 */
public class DynamicDatasource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return peek();
    }


    /**
     * 将当前线程与数据源的key进行绑定
     */
    private static final ThreadLocal<Deque<String>> CONTEXT_HOLDER = new ThreadLocal() {
        @Override
        protected Object initialValue() {
            return new ArrayDeque();
        }
    };

    /**
     * 获得当前线程数据源
     *
     * @return 数据源名称
     */
    public static String peek() {
        return CONTEXT_HOLDER.get().peek();
    }

    /**
     * 设置当前线程数据源
     *
     * @param dataSource 数据源名称
     */
    public static void push(String dataSource) {
        CONTEXT_HOLDER.get().push(dataSource);
    }

    /**
     * 清空当前线程数据源
     */
    public static void poll() {
        Deque<String> deque = CONTEXT_HOLDER.get();
        deque.poll();
        if (deque.isEmpty()) {
            CONTEXT_HOLDER.remove();
        }
    }

}

动态数据源配置类 DynamicDataSourceConfig,,这里为了演示方便,我只是做了个样例,里面具体数据源的相关配置(primary、slave)以及sqlSessionFactory,还需要大家自己定义。

package com.app.microservice.config;

import com.alibaba.druid.pool.DruidDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.ibatis.session.defaults.DefaultSqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author:whh
 * @date: 2022-04-28 21:25
 * <p></p>
 */
@Configuration
public class DynamicDataSourceConfig {


    /**
     * 主数据源
     * 这里只是做了个样例,具体的参数大家自行配置
     * @return
     */
    @Primary
    @Bean("primaryDatasource")
    public DruidDataSource primary(){
        DruidDataSource primary = new DruidDataSource();
        return primary;
    }

    /**
     * 从数据源
     * @return
     */
    @Bean("slaveDatasource")
    public DruidDataSource slave(){
        DruidDataSource slave = new DruidDataSource();
        return slave;
    }


    @Bean
    public DynamicDatasource dynamicDatasource(@Qualifier("primaryDatasource") DruidDataSource primary,
                                               @Qualifier("slaveDatasource") DruidDataSource slave){
        DynamicDatasource dynamicDatasource = new DynamicDatasource();
        //配置多数据源
        Map<Object, Object> targetDataSources = new ConcurrentHashMap<>();
        targetDataSources.put("primaryDatasource",primary);
        targetDataSources.put("slaveDatasource",slave);
        dynamicDatasource.setTargetDataSources(targetDataSources);
        //配置主数据源
        dynamicDatasource.setDefaultTargetDataSource(primary);
        //初始化父类
        dynamicDatasource.afterPropertiesSet();
        return dynamicDatasource;
    }


    /**
     * 配置sqlSessionFactory
     * @param dynamicDatasource
     * @return
     * @throws Exception
     */
    @Bean
    public SqlSessionFactory sqlSessionFactory(DynamicDatasource dynamicDatasource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dynamicDatasource);
        //配置mapper路径及配置自定义拦截器
        //...........
        return sqlSessionFactoryBean.getObject();
    }
}

以上咱们就做好了动态数据源的配置,那么接下来就是在业务代码中进行数据源的动态切换。

其实现也不复杂,具体的就是使用AOP技术,这里我们可以自定义一个注解,然后在需要用到的业务逻辑上标注该注解,并在注解参数中声明使用数据源的key,然后咱们使用AOP技术对标有该注解的方法进行拦截,然后切换到指定的数据源即可。

注解+AOP实现数据源切换

package com.app.microservice.config;

import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

/**
 * @author:whh
 * @date: 2022-04-28 21:58
 * <p></p>
 */
@Aspect
@Order(1)
@Component
public class DataSourceAspect{
    @Pointcut("@annotation(com.app.microservice.config.DataSourceAnnotation)")
    public void pointcut() {
    }

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        DataSourceAnnotation dataSourceAnnotation = method.getAnnotation(DataSourceAnnotation.class);
        if (dataSourceAnnotation != null) {
            DynamicDatasource.push(dataSourceAnnotation.value().name());
        }
        try {
            return point.proceed();
        } finally {
        //释放
            DynamicDatasource.poll();
        }
    }
}

好啦,大功告成!!!!

总结

不知到,有没有人困惑,在讲解org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource动态数据源原理时,我说determineTargetDataSource()方法将调用determineCurrentLookupKey()方法来动态获取数据源
但是determineTargetDataSource()这个方法是谁调用的呢?
其实这个问题困惑了我,虽然知道最终这个会由持久层框架mybatis来获取真正的数据源,但是mybatis里面应该不会直接是用determineTargetDataSource()这个方法来获取数据源呀,如果真是这样那耦合也太大了,,后来我在翻看AbstractRoutingDataSource这个类发现了这个两个方法,unwrap()和isWrapperFor(),发现它在这里调用了determineTargetDataSource(),并且unwrap其实是DataSource接口继承而来,,,那么这个问题就清楚了,,持久成框架获取的肯定是DataSource类型的,所以它不用关心子类到底是谁,只要你这个子类实现了DataSource接口,那么它就可以去包装获取正真的数据源。看到此我真正感受它的巧妙以及面向接口编程的强大。

在这里插入图片描述

  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值