查询mysql数据,使用动态数据源方法

146 阅读4分钟

一、添加依赖

<!--        动态数据源-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
            <version>3.2.0</version>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.4.200</version>
            <scope>runtime</scope>
        </dependency>

<!--         druid -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.6</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.6</version>
        </dependency>

二、yml添加配置

  #数据源配置
spring:
  datasource:
    master:
      jdbcUrl: jdbc:mysql://192.168.xx.xx:3306/bi?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
      username: root
      password: 123456
      driver-class-name: com.mysql.cj.jdbc.Driver
    second:
      jdbcUrl: jdbc:mysql://192.168.xx.xx:3306/duty_prod?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
      username: root
      password: 123456
      driverClassName: com.mysql.cj.jdbc.Driver
    druid:
      #连接池初始化时创建的数据库连接数量
      initial-size: 10
      #连接池中保持最小的空闲连接数量。如果空闲连接数量少于这个值,连接池会创建新的连接来补充
      min-idle: 5
      #
      max-active: 80
      #配置获取链接等待超时的时间
      max-wait: 10
    type: com.alibaba.druid.pool.DruidDataSource
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher
  jackson:
    time-zone: GMT+8

Url中的mysql服务器地址需要根据你自己的改动。

三、添加配置类

import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.hdx.biDataApi.utils.CommonCast;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class DataBaseConfig {

    @Bean("primary")
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource dataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean("secondary")
    @ConfigurationProperties(prefix = "spring.datasource.second")
    public DataSource dataSource2() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    public DataSource dynamicDataSource() {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(CommonCast.PRIMARY, dataSource());
        targetDataSources.put(CommonCast.SECONDARY, dataSource2());
        DynamicDataSource dataSource = new DynamicDataSource();
        dataSource.setTargetDataSources(targetDataSources);
        dataSource.setDefaultTargetDataSource(dataSource());
        return dataSource;
    }

    @Bean("SqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory(DataSource dynamicDataSource) throws Exception {
        MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
        sessionFactory.setDataSource(dynamicDataSource);
        MybatisConfiguration mybatisConfiguration = new MybatisConfiguration();
        sessionFactory.setConfiguration(mybatisConfiguration);
        sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mybatis/**/*.xml"));
        return sessionFactory.getObject();
    }
}
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure;
import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
import com.alibaba.druid.util.Utils;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.*;
import java.io.IOException;

/**
 * @Description: DruidConfig配置类
 */
@Configuration
@AutoConfigureAfter(DruidDataSourceAutoConfigure.class)
public class DruidConfig {

    /**
     * 带有广告的common.js全路径,druid-1.1.14
     */
    private static final String FILE_PATH = "support/http/resources/js/common.js";
    /**
     * 原始脚本,触发构建广告的语句
     */
    private static final String ORIGIN_JS = "this.buildFooter();";
    /**
     * 替换后的脚本
     */
    private static final String NEW_JS = "//this.buildFooter();";

    /**
     * 去除Druid监控页面的广告
     *
     * @param properties DruidStatProperties属性集合
     * @return {@link FilterRegistrationBean}
     */
    @Bean
    @ConditionalOnWebApplication
    @ConditionalOnProperty(name = "spring.datasource.druid.stat-view-servlet.enabled", havingValue = "true")
    public FilterRegistrationBean<RemoveAdFilter> removeDruidAdFilter(
            DruidStatProperties properties) throws IOException {
        // 获取web监控页面的参数
        DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
        // 提取common.js的配置路径
        String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*";
        String commonJsPattern = pattern.replaceAll("\*", "js/common.js");
        // 获取common.js
        String text = Utils.readFromResource(FILE_PATH);
        // 屏蔽 this.buildFooter(); 不构建广告
        final String newJs = text.replace(ORIGIN_JS, NEW_JS);
        FilterRegistrationBean<RemoveAdFilter> registration = new FilterRegistrationBean<>();
        registration.setFilter(new RemoveAdFilter(newJs));
        registration.addUrlPatterns(commonJsPattern);
        return registration;
    }

    /**
     * 删除druid的广告过滤器
     *
     * @author BBF
     */
    private class RemoveAdFilter implements Filter {

        private final String newJs;

        public RemoveAdFilter(String newJs) {
            this.newJs = newJs;
        }

        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
                throws IOException, ServletException {
            chain.doFilter(request, response);
            // 重置缓冲区,响应头不会被重置
            response.resetBuffer();
            response.getWriter().write(newJs);
        }
    }
}
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceHolder.getDataSource();
    }
}
public class DataSourceHolder {
    /**
     * 线程ThreadLocal
     */
    private static final ThreadLocal<String> dataSources = new InheritableThreadLocal();

    /**
     * 设置数据源
     * @param datasource
     */
    public static void setDataSource(String datasource) {
        dataSources.set(datasource);
    }

    /**
     * 获取数据源
     * @return
     */
    public static String getDataSource() {
        return dataSources.get();
    }

    /**
     * 清除数据源
     */
    public static void clearDataSource() {
        dataSources.remove();
    }

}

需要添加以上四个配置类

四、使用方法

在ServiceImpl实现类里,查询方法中添加
调用主数据库:
DataSourceHolder.setDataSource("primary");
调用第二个数据库:
DataSourceHolder.setDataSource("secondary");
调用主数据库可以不用特定写,即便不屑也会默认调用主数据库

public List<String> getStationsId(String generation_type) {
    //使用主数据库
    DataSourceHolder.setDataSource("primary"); 
    List<String> test1 = testService.selectStationsId(id);
    return test1;
}
public List<String> findDefectData2(List<String> stationsId) {
    //使用第二个数据库
    DataSourceHolder.setDataSource("secondary");
    List<testEntity> defectEntities = testMapper.selectList(new QueryWrapper<testEntity>()
            .in("data_id", stationsId)
    );

以上就是多数据源查询数据方法,yml配置中可以添加多个数据源,不止两个,但同样配置类中也需要对应的添加配置。

-------------------------------------------------更新----------------------------------------------------------

长时间使用多数据查询会出现高并发的问题
例如:
在使用master数据源查询时,查询还未结束,这个时候开始查询secondary数据源查询时,会出现副数据源库里的表使用了主数据源查询。

master数据源: database1库 里面有 table1,table2 secondary数据源: database2库 里面有 table3,table4

查询database1.table1 时候(高频查询)若此过程查询 secondary.table3,有概率会因为开启了database1的查询,还没释放该数据源的连接。就会报错,提示:database1.table3 doesn't exist。

这个时候需要加个mybatis全局拦截器,作用是查询结束后立马释放数据源,从而避免查询database1时候,查询完成了,但是没有释放数据源时,开始查询database2的情况。

拦截器:

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.sql.Connection;

@Component
@Intercepts({@Signature(
        type = StatementHandler.class,
        method = "prepare",
        args = {Connection.class, Integer.class}
), @Signature(
        type = StatementHandler.class,
        method = "getBoundSql",
        args = {}
), @Signature(
        type = Executor.class,
        method = "update",
        args = {MappedStatement.class, Object.class}
), @Signature(
        type = Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
), @Signature(
        type = Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}
)})
public class MyInterceptor extends MybatisPlusInterceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        try {
            return invocation.proceed();
        } finally {
            DataSourceHolder.clearDataSource();
        }
    }
}

-------------------------------------------------再次更新(2024.07.12)------------------------------------

使用拦截器后,查询操作是成功的,但如果使用插入或者更新操作,如savebatch(),就会失败,插入一条后拦截器就会清空数据源,会导致插入数据失败。这个时候需要修改拦截器,改成以下这样

@Override
public Object intercept(Invocation invocation) throws Throwable {
    try {
        return invocation.proceed();
    }
}

然后 每次数据库交换数据操作后进行手动数据源清除。例如:

DataSourceHolder.setDataSource(CommonCast.PRIMARY);
List<Entity> entities = entityMapper.selectList(new QueryWrapper<Entity>());
DataSourceHolder.clearDataSource();
return entities;

这样既可以使用多数据源,也可以进行更新或者插入操作。

以上方法若有不对的地方,欢迎指正😀