dynamic-datasource多数据源使用和源码讲解

目录

1、前置准备

2、多数据源查询切换

3、多数据源插入切换

4、多数据源事务

5、源码讲解

DynamicDataSourceAutoConfiguration.class自动装配类

DynamicRoutingDataSource数据源主要方法

DynamicDataSourceContextHolder线程上下文

@DS切面DynamicDataSourceAnnotationAdvisor

DynamicDataSourceAnnotationInterceptor拦截器

AbstractRoutingDataSource类

源码核心类

1、前置准备

依赖引入

XML 复制代码
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
            <version>3.5.2</version>
        </dependency>

测试数据库准备

2、多数据源查询切换

数据库配置文件

创建两个测试数据库来进行模拟

java 复制代码
spring:
  datasource:
    dynamic:
      primary: db1
      strict: false
      datasource:
        db1:
          url: jdbc:mysql://localhost:3306/test-db1?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
        db2:
          url: jdbc:mysql://localhost:3306/test-db2?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver

参数说明

所有的数据库配置都需要放在dynamic下,这样才能读取到

复制代码
primary:默认使用哪个库,也就是说当我们没有指定使用哪个数据库时,默认使用的就是这个

strict: 这个参数如果选择true,当我们指定错误的数据库名,就会直接报错。如果时false,那就使用默认的数据库

datasource

这个下面放列表,放的是所有数据库的连接信息

复制代码
db1、db2可以理解为数据库的别名,后面根据这个名来指定数据库

数据源切换:@DS注解

注解内接收一个参数:这个参数是用来指定使用哪个数据源的(必填)

结果

我们可以看到项目中已经实现了数据源切换

日志

我们可以通过日志查看使用了哪个库

在配置文件中添加:

java 复制代码
server:
  port: 8080

logging:
  level:
    com.baomidou.dynamic.datasource: debug

这样我们就可以看到每次使用了哪个库

3、多数据源插入切换

有时候针对一个表单提交要插入两个库,就可以来切换数据源来实现插入数据到不同的库

示例代码

测试结果

调用接口,两条数据插入到不同的库里

错误示范(同一类中调用)

此为错误示范:因为@DS注解实际还是通过AOP操作切换数据源的,所以同类中调用会失效

当我们调用时可以看到没有报错,看下日志:

这个日志是没有指定数据源使用默认数据源的提示,就是因为同类调用导致失效了。

我们也可以发现数据都插入到db1库中了

4、多数据源事务

多数据源事务,我们不再使用@Transactional而是使用@DSTransactional

注意(重要)

@DSTransactional是弱一致性,因为两个数据源的提交是分为两次的,如果发生网络异常、宕机,可能就会导致回滚失败,因为提交多次非原子性

5、源码讲解

在学习源码前我们可以理解下mybatis是怎么发起连接数据库的,是通过注入一个DataSource对象,通过这个对象的getConnection对象来获取数据库连接的

下面开始讲解dynamic多数据源实现源码

1、数据源读取

在我们引入dynamic-datasource-spring-boot-starter依赖时,会引入一个自动装配类

DynamicDataSourceAutoConfiguration.class自动装配类

这个类会读取配置中的所有数据源,封装成一个Map对象,map的key存储的是我们放的数据源名,value就是一个个的DataSource数据源对象(DataSource中存的就是数据库的连接信息)

从这里可以看到读取的就是spring.datasource.dynamic下的配置信息

java 复制代码
@Bean
    @Order(0)
    public DynamicDataSourceProvider ymlDynamicDataSourceProvider() {
        return new YmlDynamicDataSourceProvider(this.properties.getDatasource());
    }

这个Bean的作用就是加载配置文件中的所有配置信息返回一个DynamicDataSourceProvider对象,入参是:

复制代码
this.properties.getDatasource()

这个是获取所有的数据源配置信息

为什么要返回DynamicDataSourceProvider对象,因为在构建数据源对象DynamicRoutingDataSource要用,也就是重点

这个bean是一个重点!!前面说的mybatis是通过datasource来获取连接的,这里就返回了一个datasource对象,我们可以具体看下这个bean都做了什么

java 复制代码
@Bean
    @ConditionalOnMissingBean
    public DataSource dataSource() {
        DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource();
        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());
        return dataSource;
    }

DynamicRoutingDataSource数据源主要方法

这个DynamicRoutingDataSource对象有什么用呢?这个其实也是一个DataSource的实现子类,只不过现在它可以理解为一个路由。

这个对象有方法加载了所有的数据源信息,并且有方法可以根据数据库名返回对应的数据源

这个对象中存了项目中定义的数据源信息:

复制代码
private final Map<String, DataSource> dataSourceMap = new ConcurrentHashMap();

具体方法:

afterPropertiesSet()

java 复制代码
public void afterPropertiesSet() throws Exception {
        this.checkEnv();
        Map<String, DataSource> dataSources = new HashMap(16);
        Iterator var2 = this.providers.iterator();

        while(var2.hasNext()) {
            DynamicDataSourceProvider provider = (DynamicDataSourceProvider)var2.next();
            dataSources.putAll(provider.loadDataSources());
        }

        var2 = dataSources.entrySet().iterator();

        while(var2.hasNext()) {
            Map.Entry<String, DataSource> dsItem = (Map.Entry)var2.next();
            this.addDataSource((String)dsItem.getKey(), (DataSource)dsItem.getValue());
        }

        if (this.groupDataSources.containsKey(this.primary)) {
            log.info("dynamic-datasource initial loaded [{}] datasource,primary group datasource named [{}]", dataSources.size(), this.primary);
        } else if (this.dataSourceMap.containsKey(this.primary)) {
            log.info("dynamic-datasource initial loaded [{}] datasource,primary datasource named [{}]", dataSources.size(), this.primary);
        } else {
            log.warn("dynamic-datasource initial loaded [{}] datasource,Please add your primary datasource or check your configuration", dataSources.size());
        }

    }

这个方法是bean生命周期中的初始化时的操作,加载所有的数据源到Map<String, DataSource> dataSourceMap对象中,通过前面的DynamicDataSourceProvider对象来获取

addDataSource(String ds, DataSource dataSource)

添加数据源到Map中,上面方法有调用

java 复制代码
public synchronized void addDataSource(String ds, DataSource dataSource) {
        DataSource oldDataSource = (DataSource)this.dataSourceMap.put(ds, dataSource);
        this.addGroupDataSource(ds, dataSource);
        if (oldDataSource != null) {
            this.closeDataSource(ds, oldDataSource);
        }

        log.info("dynamic-datasource - add a datasource named [{}] success", ds);
    }

getDataSource(String ds)

这个方法是重点,这个方法实际就是返回一个DataSource供mybatis使用的,正如前面说的mybatis会调用当前项目中的DataSource对象的getConnect方法来获取数据库连接,而我们这个方法正是返回了一个DataSource。

这个方法的入参是ds,也就是我们的数据源名称,通过数据源名称从所有数据源的map中获取对象的DataSource对象

可以看到里面打印的日志log.debug也正是我们前面测试打印的

java 复制代码
public DataSource getDataSource(String ds) {
        if (StringUtils.isEmpty(ds)) {
            return this.determinePrimaryDataSource();
        } else if (!this.groupDataSources.isEmpty() && this.groupDataSources.containsKey(ds)) {
            log.debug("dynamic-datasource switch to the datasource named [{}]", ds);
            return ((GroupDataSource)this.groupDataSources.get(ds)).determineDataSource();
        } else if (this.dataSourceMap.containsKey(ds)) {
            log.debug("dynamic-datasource switch to the datasource named [{}]", ds);
            return (DataSource)this.dataSourceMap.get(ds);
        } else if (this.strict) {
            throw new CannotFindDataSourceException("dynamic-datasource could not find a datasource named" + ds);
        } else {
            return this.determinePrimaryDataSource();
        }
    }

DynamicDataSourceContextHolder线程上下文

这个上下文就是来定义什么时候使用哪个数据源

java 复制代码
private static final ThreadLocal<Deque<String>> LOOKUP_KEY_HOLDER = new NamedThreadLocal<Deque<String>>("dynamic-datasource") {
        protected Deque<String> initialValue() {
            return new ArrayDeque();
        }
    };

主要有以下方法:

复制代码
String push(String ds)将当前线程使用的数据源的名称存入线程中
java 复制代码
public static String push(String ds) {
        String dataSourceStr = StringUtils.isEmpty(ds) ? "" : ds;
        ((Deque)LOOKUP_KEY_HOLDER.get()).push(dataSourceStr);
        return dataSourceStr;
    }
复制代码
String peek()获取当前线程中使用的数据源名称
java 复制代码
public static String peek() {
        return (String)((Deque)LOOKUP_KEY_HOLDER.get()).peek();
    }
java 复制代码
public static void clear() {
        LOOKUP_KEY_HOLDER.remove();
    }
java 复制代码
public static void poll() {
        Deque<String> deque = (Deque)LOOKUP_KEY_HOLDER.get();
        deque.poll();
        if (deque.isEmpty()) {
            LOOKUP_KEY_HOLDER.remove();
        }

    }

@DS切面DynamicDataSourceAnnotationAdvisor

定义要拦截带有@DS注解的类或者方法,交给DynamicDataSourceAnnotationInterceptor拦截器处理

DynamicDataSourceAnnotationInterceptor拦截器

这里是真正操作实现切换数据源的地方

determineDatasourceKey(MethodInvocation invocation)

这个方法是获取注解上的数据源的名称,也就是@DS(value)中value值

java 复制代码
private String determineDatasourceKey(MethodInvocation invocation) {
        String key = this.dataSourceClassResolver.findKey(invocation.getMethod(), invocation.getThis());
        return key.startsWith("#") ? this.dsProcessor.determineDatasource(invocation, key) : key;
    }

invoke(MethodInvocation invocation)

这个方法是将当前要是使用的线程上下文设置到数据源的名称存入线程中和使用完之后清理

java 复制代码
 public Object invoke(MethodInvocation invocation) throws Throwable {
        String dsKey = this.determineDatasourceKey(invocation);
        DynamicDataSourceContextHolder.push(dsKey);

        Object var3;
        try {
            var3 = invocation.proceed();
        } finally {
            DynamicDataSourceContextHolder.poll();
        }

        return var3;
    }

AbstractRoutingDataSource类

这个类是上面的DynamicRoutingDataSource的父类,也正如我们前面说的mybatis获取数据库连接是通过DataSource方法的getConnect方法来获取的,这个方法中就定义了

getConnection()

java 复制代码
public Connection getConnection() throws SQLException {
        String xid = TransactionContext.getXID();
        if (StringUtils.isEmpty(xid)) {
            return this.determineDataSource().getConnection();
        } else {
            String ds = DynamicDataSourceContextHolder.peek();
            ds = StringUtils.isEmpty(ds) ? this.getPrimary() : ds;
            ConnectionProxy connection = ConnectionFactory.getConnection(ds);
            return (Connection)(connection == null ? this.getConnectionProxy(ds, this.determineDataSource().getConnection()) : connection);
        }
    }

通过获取线程上下文中的使用的数据源,如果数据源为空,就用默认数据源。

this.determineDataSource()获取数据源对象

这个方法是这个AbstractRoutingDataSource类中的抽象方法,子类DynamicRoutingDataSource实现了这个方法

java 复制代码
public DataSource determineDataSource() {
        String dsKey = DynamicDataSourceContextHolder.peek();
        return this.getDataSource(dsKey);
    }

至此形成调用闭环

源码核心类

复制代码
DynamicDataSourceAutoConfiguration
复制代码
DynamicRoutingDataSource
复制代码
AbstractRoutingDataSource
复制代码
DynamicDataSourceAnnotationInterceptor
复制代码
DynamicDataSourceAnnotationAdvisor
相关推荐
草莓熊Lotso2 小时前
MySQL 多表连接查询实战:内连接 + 外连接
android·运维·数据库·c++·mysql
杨校2 小时前
杨校老师课堂备战C++之数据结构中栈结构专题训练
开发语言·数据结构·c++
无籽西瓜a2 小时前
【西瓜带你学设计模式 | 第一期-单例模式】单例模式——定义、实现方式、优缺点与适用场景以及注意事项
java·后端·单例模式·设计模式
倔强的石头1062 小时前
文档数据库迁移实战:MongoDB 协议级兼容与 JSONB 引擎性能深度对比
数据库·mongodb·kingbase
wefly20172 小时前
m3u8live.cn:免安装 HLS 在线播放器,流媒体调试效率神器
开发语言·javascript·python·django·ecmascript·hls.js 原理·m3u8 解析
J_liaty2 小时前
JavaScript 基础知识全解析:从入门到精通
开发语言·javascript
2301_816651222 小时前
C++与Rust交互编程
开发语言·c++·算法
空空潍2 小时前
Spring AI 实战系列(二):ChatClient封装,告别大模型开发样板代码
java·人工智能·spring
he___H2 小时前
mongodb
数据库·mongodb