问题

1、数据不都是在一个库存在,可能需要到其他库去操作数据

比如现在需要从两个不同库里去拿user数据

springboot 输入流如何构造formdata springboot数据流转_Source

而我们用springboot项目做配置一般是这么写的:

springboot 输入流如何构造formdata springboot数据流转_Source_02

其中url已经定位到一个库了,所有的操作都是基于这个test库里面的不同的表

2、公司业务做得比较大,数据库需要主从分离

基于以上两种问题,需要做数据源的动态切换。

解决方案

1、思路分析

要想配置多个数据源,必须新增配置文件。(你不写配置,系统怎么知道)

要有地方能够配置多数据源。(读出配置,告诉系统)

要能够选择用什么数据源。(系统知道有哪些,但是要我们自己指定用哪个)

2、实现

关键点:

  • 配置文件application.yml新增另一个库的配置
spring:
  datasource:
    name: mybatis-test
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      url: jdbc:mysql://127.0.0.1:3306/test
      username: root
      password: root
      driver-class-name: com.mysql.jdbc.Driver
  datasource-ext:
    name: mybatis-test-ext
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      url: jdbc:mysql://127.0.0.1:3306/oauth2
      username: root
      password: root
      driver-class-name: com.mysql.jdbc.Driver
  • 继承AbstractRoutingDataSource,重写determineCurrentLookUpKey方法
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * @author zab
 * @date 2019/5/30 11:31
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DatabaseContextHolder.getDatabaseType();
    }

    public static class DatabaseContextHolder {
        private static final ThreadLocal<DatabaseType> CONTEXT_HOLDER = new ThreadLocal<>();

        public static DatabaseType getDatabaseType() {
            return CONTEXT_HOLDER.get();
        }

        public static void setDatabaseType(DatabaseType type) {
            CONTEXT_HOLDER.set(type);
        }
    }
}
  • 新增JavaConfig,把配置文件中的配置告诉系统
import com.alibaba.druid.pool.DruidDataSourceFactory;
import com.changan.dynamicdatasource.common.datasource.DatabaseType;
import com.changan.dynamicdatasource.common.datasource.DynamicDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
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 org.springframework.core.env.Environment;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;

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

/**
 * springboot集成mybatis的基本入口
 * 1)创建数据源(如果采用的是默认的tomcat-jdbc数据源,则不需要)
 * 2) 创建SqlSessionFactory
 * 3)配置事务管理器,除非需要使用事务,否则不用配置
 */
@Configuration
@MapperScan(basePackages = "com.changan.dynamicdatasource.dao")
public class MyBatisConfig {

    @Autowired
    private Environment env;

    /**
     * 创建数据源(数据源的名称:方法名可以取为XXXDataSource(),XXX为数据库名称,该名称也就是数据源的名称)
     * @Primary 该注解表示在同一个接口有多个实现类可以注入的时候,默认选择哪一个,而不是让@Autowire注解报错
     */
    @Bean
    @Primary
    public DataSource myDataSource1() throws Exception {
        System.out.println(env.getProperty("spring.datasource.druid.driver-class-name"));
        Properties props = new Properties();
        props.put("driverClassName", env.getProperty("spring.datasource.druid.driver-class-name"));
        props.put("url", env.getProperty("spring.datasource.druid.url"));
        props.put("username", env.getProperty("spring.datasource.druid.username"));
        props.put("password", env.getProperty("spring.datasource.druid.password"));
        return DruidDataSourceFactory.createDataSource(props);
    }

    @Bean
    public DataSource myDataSource2() throws Exception {
        Properties props = new Properties();
        props.put("url", env.getProperty("spring.datasource-ext.druid.url"));
        props.put("driverClassName", env.getProperty("spring.datasource-ext.druid.driver-class-name"));
        props.put("username", env.getProperty("spring.datasource-ext.druid.username"));
        props.put("password", env.getProperty("spring.datasource-ext.druid.password"));
        return DruidDataSourceFactory.createDataSource(props);
    }

    /**
     *
     * @Qualifier 根据名称进行注入,通常是在具有相同的多个类型的实例的一个注入(例如有多个DataSource类型的实例)
     */
    @Bean
    public DynamicDataSource dataSource(@Qualifier("myDataSource1") DataSource myTestDbDataSource1,
                                        @Qualifier("myDataSource2") DataSource myTestDbDataSource2) {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DatabaseType.db1, myTestDbDataSource1);
        targetDataSources.put(DatabaseType.db2, myTestDbDataSource2);

        DynamicDataSource dataSource = new DynamicDataSource();
        // 该方法是AbstractRoutingDataSource的方法
        dataSource.setTargetDataSources(targetDataSources);
        // 默认的datasource设置为myTestDbDataSource
        dataSource.setDefaultTargetDataSource(myTestDbDataSource1);

        return dataSource;
    }

    /**
     * 根据数据源创建SqlSessionFactory
     */
    @Bean
    public SqlSessionFactory sqlSessionFactory(DynamicDataSource ds) throws Exception {
        SqlSessionFactoryBean fb = new SqlSessionFactoryBean();
        // 指定数据源(这个必须有,否则报错)
        fb.setDataSource(ds);
        // 下边两句仅仅用于*.xml文件,如果整个持久层操作不需要使用到xml文件的话(只用注解就可以搞定),则不加
        // 指定基包
        fb.setTypeAliasesPackage(env.getProperty("mybatis.type-aliases-package"));
        fb.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(env.getProperty("mybatis.mapper-locations")));

        return fb.getObject();
    }

    /**
     * 配置事务管理器
     */
    @Bean
    public DataSourceTransactionManager transactionManager(DynamicDataSource dataSource) throws Exception {
        return new DataSourceTransactionManager(dataSource);
    }

}
  • 选择用什么数据源
public Msg getAllUserInfo() {
        DynamicDataSource.DatabaseContextHolder.setDatabaseType(DatabaseType.db1);
        List<User> userList = userMapper.getAllUserInfo();
        return Msg.success("ok", userList);
}
public Msg getAllOrderInfo() {
        DynamicDataSource.DatabaseContextHolder.setDatabaseType(DatabaseType.db2);
        List<Order> orderList = orderMapper.getAllOrderInfo();
        return Msg.success("ok", orderList);
}

第一步配置不用过多描述

第二步中用到了ThreadLocal,这个是面试常考题,配置多数据源是一个应用点,ThreadLocal其实就是ThreadLocalVariable

即线程本地变量,提供了线程之间变量隔离,同线程变量共享的能力。

第三步中,利用了AbstractRoutingDataSource的方法,指定了默认数据源和所有数据源(Map)

Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DatabaseType.db1, myTestDbDataSource1);
targetDataSources.put(DatabaseType.db2, myTestDbDataSource2);

DynamicDataSource dataSource = new DynamicDataSource();
// 该方法是AbstractRoutingDataSource的方法
dataSource.setTargetDataSources(targetDataSources);
// 默认的datasource设置为myTestDbDataSource
dataSource.setDefaultTargetDataSource(myTestDbDataSource1);

第四步中模拟了简单的查询功能,假定有订单和用户两张表存在于不同的库,在查询之前需要选择数据库,即:

DynamicDataSource.DatabaseContextHolder.setDatabaseType(DatabaseType.db1);

这里选择后,在AbstractRoutingDataSource类中,detemineTargetDataSource方法取出对应的配置类(Properties)

当然取出操作是我们在service层指定数据源类型后,在链接数据库时,系统自发获取配置的,我们无需编码实现。这里也看出ThreadLocal的作用。

第四步选择数据源,如果没添加一个方法就选一次,会造成大量的横向代码冗余,因此我们可以用aop做切面抽出,用注解标记,然后拦截带有特定注解的方法。

springboot 输入流如何构造formdata springboot数据流转_数据源_03

案例github地址
https://github.com/Jackson-zhanganbing/dynamic-datasource.git