DynamicDatasource动态数据源实现及使用

动态数据源DynamicDatasource

简介

本文介绍baomidou开源的多数据源扩展插件DynamicDatasource,并用来实现动态数据源及以下功能

介绍功能包括:

  1. 提供 自定义数据源来源 方案(如全从数据库加载)。
  2. 提供项目启动后 动态增加移除数据源 方案。
  3. 支持 自定义注解 ,需继承DS(3.2.0+)。

DynamicDatasource项目地址文档地址

本文Demo完整源码:
Github源代码地址:https://github.com/xunfeng224/Springboot/tree/main/springboot-DynamicDatasource
Gitee源代码地址:https://gitee.com/xfeng520/Springboot/tree/main/springboot-DynamicDatasource

[!CAUTION]

运行源码需注意,由于加入了手动实现的动态数据源,会导致Bean冲突,若想运行DynamicDataSource,将手动代码dynamic包直接删除,若想运行手动实现的动态数据源,将LoadDataSourceRunner类全部注释掉,避免找不到Bean报错。手动动态数据源相关代码全部位于dynamic包下。

原因:自定义数据源管理类DynamicDataSource继承了Spring的AbstractRoutingDataSource类,而在DynamicDataSource源码中,DynamicRoutingDataSource类同样继承了 AbstractRoutingDataSource ,本来想通过自定义Bean注入名称来解决Bean冲突,发现不可行。在DynamicDataSourceAutoConfiguration中注册DynamicRoutingDataSource的bean时,加入了@ConditionalOnMissingBean注解,这将导致有自定义实现类bean注入Spring容器时,DynamicRoutingDataSource无法注入Spring容器,从而启动报错。

@ConditionalOnMissingBean作用:判断当前需要注入Spring容器中的bean的实现类是否已经含有,有的话不注入,没有就注入

    @Bean
    @ConditionalOnMissingBean
    public DataSource dataSource(List<DynamicDataSourceProvider> providers) {
    
        DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource(providers);
        dataSource.setPrimary(this.properties.getPrimary());
        dataSource.setStrict(this.properties.getStrict());
        dataSource.setStrategy(this.properties.getStrategy());
        dataSource.setP6spy(this.properties.getP6spy());
        dataSource.setSeata(this.properties.getSeata());
        dataSource.setGraceDestroy(this.properties.getGraceDestroy());
        return dataSource;
    }

源码分析

ThreadLocal和AbstractRoutingDataSource

ThreadLocal:全称:thread local variable。主要是为解决多线程时由于并发而产生数据不一致问题。ThreadLocal为每个线程提供变量副本,确保每个线程在某一时间访问到的不是同一个对象,这样做到了隔离性,增加了内存,但大大减少了线程同步时的性能消耗,减少了线程并发控制的复杂程度。

  • ThreadLocal作用:在一个线程中共享,不同线程间隔离
  • ThreadLocal原理:ThreadLocal存入值时,会获取当前线程实例作为key,存入当前线程对象中的Map中。

AbstractRoutingDataSource:根据用户定义的规则选择当前的数据源,

Spring boot提供了AbstractRoutingDataSource 根据用户定义的规则选择当前的数据源,这样我们可以在执行查询之前,设置使用的数据源。实现可动态路由的数据源,在每次数据库查询操作前执行。它的抽象方法 determineCurrentLookupKey() 决定使用哪个数据源。

DynamicRoutingDataSource

该类继承上述提到的AbstractRoutingDataSource抽象类,实现determineDataSource()方法,如上文所述,该方法决定了当前数据库操作所使用的数据源

public class DynamicRoutingDataSource extends AbstractRoutingDataSource implements InitializingBean, DisposableBean {
   
 //...省略...   
     /**
     * 通过各种方式加载的数据源将存储在该Map中,后续动态切换也是从这里获取
     */
    private final Map<String, DataSource> dataSourceMap = new ConcurrentHashMap<>();
        /**
     * 分组数据库
     */
    private final Map<String, GroupDataSource> groupDataSources = new ConcurrentHashMap<>();
    /**
    获取主数据源
    */
    @Override
    protected String getPrimary() {
   
        return primary;
    }

    @Override
    public DataSource determineDataSource() {
   
        // 数据源key/数据源名称从DynamicDataSourceContextHolder.peek()中获取
        String dsKey = DynamicDataSourceContextHolder.peek();
        return getDataSource(dsKey);
    }
    
      /**
     * 获取数据源
     *
     * @param ds 数据源名称
     * @return 数据源
     */
    public DataSource getDataSource(String ds) {
   
        if (DsStrUtils.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 CannotFindDataSourceException("dynamic-datasource could not find a datasource named " + ds);
        }
        return determinePrimaryDataSource();
    }
    
        /**
     * 添加数据源
     *
     * @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) {
   
            closeDataSource(ds, oldDataSource, graceDestroy);
        }
        log.info("dynamic-datasource - add a datasource named [{}] success", ds);
    }
 //...省略...  
    
}

DynamicDataSourceContextHolder类 ,源码自带注解也挺详细的了,不做多解释

import org.springframework.core.NamedThreadLocal;

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

/**
 * 核心基于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 数据源名称
     * @return 数据源名称
     */
    public static String push(String ds) {
   
        String dataSourceStr = DsStrUtils.isEmpty(ds) ? "" : ds;
        LOOKUP_KEY_HOLDER.get().push(dataSourceStr);
        return dataSourceStr;
    }

    /**
     * 清空当前线程数据源
     * <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();
    }
}

DynamicDatasource快速开始

其实也没啥好写的,DynamicDatasource功能很丰富,但本文章只涉及到简单的操作。流程为新建Springboot项目,引入Maven依赖,配置yml中的master数据源,使用mybatis-plus快速实现查询主数据源sys_user表数据,通过defaultDataSourceCreator.createDataSource(dataSourceProperty)创建数据源,通过dynamicRoutingDataSource.addDataSource(ds.getId().toString(), dataSource);添加数据源,通过DynamicDataSourceContextHolder.push(dsId.toString());切换数据源或通过注解@DS(“master”)切换

项目结构

请添加图片描述

Maven依赖

引入DynamicDatasource依赖

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
            <version>4.3.1</version>
        </dependency>

引入其他依赖,为本案例项目所需依赖,非实现动态数据源所必须

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.22</version>
        </dependency>
        <dependency
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值