dynamic-datasource源码分析

问题

  • 为什么要实现数据源动态切换?

    首先我们的项目是集成MyBatis或MyBatis-Plus的。当我们每次执行sql操作方法时,MyBatis或MP会从数据源里拿到一个数据库连接和数据库通信并执行sql。当多个数据源时要保证sql能到对应的数据库上,要求我们做到每一个不同数据源的sql都要拿到对应的数据源并获取相应数据库连接。

  • dynamic-datasource怎么实现数据源动态切换?

    1. 多数据源集合: 既然需要从不同数据源拿数据库连接,那怎么获取数据源呢?只要你拿着数据源名称来,我根据名称提供给你数据源就好了,所以我们可以在路由类里维护一个Map集合,key为数据源名称,value为数据源。
    2. 注解+切面:通过自定义注解加切面,就可以拿到每个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();
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值