SpringBoot动态多数据源

1.简介

SpringBoot静态数据源指的是将多个数据源信息配置在配置文件中,在项目启动时加载配置文件中的多个数据源,并实例化多个数据源Bean,再通过分包/Aop达到切换数据源的目的

如果想要新增或者修改数据源,必须修改配置文件,并修改对应的代码(增加对应的DataSource Bean)重启项目,重新实例化数据源,才能使用

动态数据源指的是将数据源信息配置在关系型数据库中或者缓存数据库中,在项目启动时只初始化一个默认数据源,在项目运行过程中动态的从数据库中读取数据源信息,实例化为DataSource,并使用该数据源获取连接,执行SQL

此处研究的ORM框架为Mybatis

2.实现方法

动态数据源的实现方法有两种

2.1 重写DataSource中的getConnection方法

Mybatis中数据库的连接都是在执行sql的时候才触发并创建连接

由此可以通过重写数据源的getConnection()方法,当Mybatis需要创建对应的数据库连接时,跟据要使用的数据源,修改当前使用的数据源,达到动态数据源的目的

2.2 通过AbstractRoutingDataSource类

AbstractRoutingDataSource类是jdbc提供的轻量级的切换数据源的方案,内部维护了一个数据源的集合,提供了维护这个数据源集合的方法,这样我们在动态的创建完对应的数据源后,就可以通过这个类提供的方法,将数据源维护进这个集合,再通过重写 determineCurrentLookupKey()方法,来告诉 AbstractRoutingDataSource我们要的是哪个数据源,最终达到切换数据源的目的

3.具体代码

3.1 方案1:重写DataSource中的getConnection方法

3.1.1 主配置类 DataSourceConfig
@Configuration
@MapperScan(basePackages = "com.zhangyao.springboot.mapper",sqlSessionFactoryRef = "sqlSessionFactory")
public class DataSourceConfig {

    /**
     * 主数据源配置
     * @return
     */
    @Bean(name = "primaryDataSource")
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource.test1")
    public DataSource getDataSource1(){
        HikariDataSource datasource =  DataSourceBuilder.create().type(MyDynamicDataSource.class).build();
        if(datasource==null){
            datasource = new MyDynamicDataSource().initDataSource("default");
        }
        //设置默认的数据源
        DataSourceCache.put("default", datasource);
        ThreadLocalDataSource.setLocalSource("default");
        return datasource;
    }
    @Bean("sqlSessionFactory")
    @Primary
    public SqlSessionFactory getSqlSessionFactory(@Qualifier("primaryDataSource") DataSource primaryDataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(primaryDataSource);
        return sqlSessionFactoryBean.getObject();
    }
}

这个类里配置了默认的数据源default,这个数据源是从配置文件中读取到的,并且实例化了Mybatis的SqlSessionFactory,默认注入default数据源

3.1.2 MyDynamicDataSource 覆盖HikariDataSource的getConnection()
package com.zhangyao.springboot.config;

import com.zaxxer.hikari.HikariDataSource;
import com.zhangyao.springboot.domin.Databaseinfo;
import com.zhangyao.springboot.service.DataBaseService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.core.io.support.PropertiesLoaderUtils;

import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author: zhangyao
 * @create:2020-11-03 21:07
 * @Description:
 **/
public class MyDynamicDataSource extends HikariDataSource {

    @Autowired
    DataBaseService dataBaseService;



    /**
     * 定义缓存数据源的变量
     */
    public static final Map<Object, Object> DataSourceCache = new ConcurrentHashMap<Object, Object>();

    @Override
    public Connection getConnection() throws SQLException {
        String localSourceKey = ThreadLocalDataSource.getLocalSource();
        HikariDataSource dataSource = (HikariDataSource) DataSourceCache.get(localSourceKey);
        if(dataSource==null){
	        try {
	            dataSource = initDataSource(localSourceKey);
	        } catch (IOException e) {
	            e.printStackTrace();
	        }
        }
        return dataSource.getConnection();
    }

    /**
     * 初始化DataSource
     * 当缓存中没有对应的数据源时,需要去默认数据源查询数据库
     *
     * @param key
     * @return
     */
    public HikariDataSource initDataSource(String key) throws IOException {
        HikariDataSource dataSource = new HikariDataSource();
        if ("default".equals(key)) {
            Properties properties = PropertiesLoaderUtils.loadProperties(new EncodedResource(new ClassPathResource("application.properties"), "UTF-8"));
            dataSource.setJdbcUrl(properties.getProperty("spring.datasource.test1.jdbc-url"));
            dataSource.setUsername(properties.getProperty("spring.datasource.test1.username"));
            dataSource.setPassword(properties.getProperty("spring.datasource.test1.password"));
            dataSource.setDriverClassName(properties.getProperty("spring.datasource.test1.driver-class-name"));
        } else {
            //查询数据库
            ThreadLocalDataSource.setLocalSource("default");
            Databaseinfo dataBaseInfo = dataBaseService.getDataBaseInfo(key);
            dataSource.setJdbcUrl(dataBaseInfo.getUrl());
            dataSource.setUsername(dataBaseInfo.getUserName());
            dataSource.setPassword(dataBaseInfo.getPassword());
            dataSource.setDriverClassName(dataBaseInfo.getDriverClassName());
            ThreadLocalDataSource.setLocalSource(key);
        }
        DataSourceCache.put(key, dataSource);
        return dataSource;
    }
}

这个类重写了HikariDatasource类的getConnection()方法,当Mybatis使用连接时,就会调用MyDynamicDataSource的getConnection()方法,然后通过获取ThreadLoacal中存放的当前使用的数据源的key,进而从自定义的缓存变量 DataSourceCache 中获取对应的数据源,如果获取不到,就使用默认数据源查询数据库,如果再获取不到,就抛出异常,查询到数据源后,初始化完再放入到缓存中DataSourceCache

3.1.3 ThreadLocalDataSource 存放当前线程使用数据源的key
package com.zhangyao.springboot.config;

import lombok.extern.slf4j.Slf4j;

import javax.xml.crypto.Data;

/**
 * ThreadLocal保存数据源的key,并切换清除
 * @author: zhangyao
 * @create:2020-04-07 09:24
 **/
@Slf4j
public class ThreadLocalDataSource {

    //使用threadLocal保证切换数据源时的线程安全 不会在多线程的情况下导致切换错数据源
    private static final ThreadLocal<String> TYPE = new ThreadLocal<String>();

    /**
     * 修改当前线程内的数据源id
     * @param key
     */
    public static void setLocalSource(String key){
        TYPE.set(key);
    }

    /**
     * 获取当前线程内的数据源类型
     * @return
     */
    public static String getLocalSource(){
        return TYPE.get();
    }

    /**
     * 清空ThreadLocal中的TYPE
     */
    public void clear(){
        TYPE.remove();
    }

}

提供了对ThreadLocal的set/get操作方法,ThreadLocal中存放的是数据源的key,这个key与 MyDynamicDataSource中的DataSourceCache中的key一致,ThreadLocal的作用就是保证在当前线程内可以取到唯一的数据源的key

3.1.4 DataSourceAop 切面 用于解析请求中的数据源的key
@Aspect
@Component
@Slf4j
public class DataSourceAop {
    /**
     * 定义切入点
     * 切入点为有该注解的方法
     * 此注解用于数据源TEST1
     */
    @Pointcut("@annotation(com.zhangyao.springboot.annotation.DataSourceServiceAop)")
    public void serviceTest1DatasourceAspect(){};

    /**
     * 在切入service方法之前执行
     * 设置数据源
     */
    @Before("serviceTest1DatasourceAspect()")
    public void beforeAspect(){
        log.info("切入方法,开始设置数据源");
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        String database_key = attributes.getRequest().getHeader("database_key");
        ThreadLocalDataSource.setLocalSource(database_key);


    }
    /**
     * 在切入service方法之后执行
     * 设置回默认数据源
     */
    @After("serviceTest1DatasourceAspect()")
    public void afterAspect(){
        log.info("切入方法后,开始切换默认数据源");
        ThreadLocalDataSource.setLocalSource("default");
    }
}

需要自定义一个注解@DataSourceServiceAop,标识在使用动态数据源的方法上

这里是跟据前台传输的数据源的key来设置ThreadLocal中的key,如果前台传输的数据源的key不在header中,再跟据实际情况调整

切完方法之后,切换回default数据源

3.2 方案2: 通过AbstractRoutingDataSource类

3.2.1 主配置类 DataSourceConfig
package com.zhangyao.springboot.config;

import com.alibaba.druid.pool.DruidDataSource;
import com.zaxxer.hikari.HikariDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Qualifier;
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.context.annotation.Primary;
import tk.mybatis.spring.annotation.MapperScan;

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

import static com.zhangyao.springboot.config.MyDynamicDataSource.DataSourceCache;


/**
 *
 * aop多数据源动态切换配置
 * @author: zhangyao
 * @create:2020-04-06 22:17
 **/

@Configuration
@MapperScan(basePackages = "com.zhangyao.springboot.mapper",sqlSessionFactoryRef = "sqlSessionFactory")
public class DataSourceConfig {

    /**
     * 主数据源配置
     * @return
     */
    @Bean(name = "primaryDataSource")
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource.test1")
    public DataSource getDataSource1(){
        HikariDataSource datasource =  DataSourceBuilder.create().type(HikariDataSource.class).build();
        //设置默认的数据源
        DataSourceCache.put("default", datasource);
        ThreadLocalDataSource.setLocalSource("default");
        return datasource;
    }
    /**
     * 动态装配所有的数据源
     * @param primaryDataSource
     * @return
     */
    @Bean("dynamicDataSource")
    public DynamicChangeDataSourceConfig setDynamicDataSource(@Qualifier("primaryDataSource") DataSource primaryDataSource){
        //定义所有的数据源
        Map<Object,Object> allDataSource = new HashMap<Object, Object>();
        //把配置的多数据源放入map
        allDataSource.put("default", primaryDataSource);

        //定义实现了AbstractDataSource的自定义aop切换类
        DynamicChangeDataSourceConfig dynamicChangeDataSourceConfig = new DynamicChangeDataSourceConfig();
        //把上面的所有的数据源的map放进去
        dynamicChangeDataSourceConfig.setTargetDataSources(allDataSource);
        //设置默认的数据源
        dynamicChangeDataSourceConfig.setDefaultTargetDataSource(primaryDataSource);

        return dynamicChangeDataSourceConfig;
    }

    @Bean("sqlSessionFactory")
    @Primary
    public SqlSessionFactory getSqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dynamicDataSource);
        return sqlSessionFactoryBean.getObject();
    }

}

与方案一一样需要先实例化默认数据源default,但是MyBatis的SqlSessionFactory中注入的就不是默认数据源了,而是AbstractRoutingDataSource的实现类,并将默认数据源放入AbstractRoutingDataSource的targetDataSources中

3.2.2 DynamicChangeDataSourceConfig AbstractRoutingDataSource的子类
package com.zhangyao.springboot.config;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * 继承AbStractRoutingDataSource
 * 动态切换数据源
 * @author: zhangyao
 * @create:2020-04-07 09:23
 **/
public class DynamicChangeDataSourceConfig extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return ThreadLocalDataSource.getLocalSource();
    }

}

重写了AbstractRoutingDataSource的determineCurrentLookupKey方法,改为返回ThreadLocal中的数据源的key

3.2.3 ThreadLocalDataSource 存放当前线程使用数据源的key

与方案1一摸一样

3.2.3 DataSourceAop 切面 用于解析请求中的数据源的key
package com.zhangyao.springboot.config;

import com.zaxxer.hikari.HikariDataSource;
import com.zhangyao.springboot.domin.Databaseinfo;
import com.zhangyao.springboot.service.DataBaseService;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;

import static com.zhangyao.springboot.config.MyDynamicDataSource.DataSourceCache;

/**
 * @author: zhangyao
 * @create:2020-04-07 11:20
 **/
@Aspect
@Component
@Slf4j
public class DataSourceAop {
    @Autowired
    DataBaseService dataBaseService;

    @Resource(name = "dynamicDataSource")
    DynamicChangeDataSourceConfig dynamicChangeDataSourceConfig;
    /**
     * 定义切入点
     * 切入点为有该注解的方法
     * 此注解用于数据源TEST1
     */
    @Pointcut("@annotation(com.zhangyao.springboot.annotation.DataSourceServiceAop)")
    public void serviceTest1DatasourceAspect(){};

    /**
     * 在切入service方法之前执行
     * 设置数据源
     */
    @Before("serviceTest1DatasourceAspect()")
    public void beforeAspect(){
        log.info("切入方法,开始设置数据源");
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        String database_key = attributes.getRequest().getHeader("database_key");
        initDataSource(database_key);
        ThreadLocalDataSource.setLocalSource(database_key);


    }
    /**
     * 在切入service方法之后执行
     * 设置回默认数据源
     */
    @After("serviceTest1DatasourceAspect()")
    public void afterAspect(){
        log.info("切入方法后,开始切换默认数据源");
        ThreadLocalDataSource.setLocalSource("default");
    }


    public HikariDataSource initDataSource(String key) {
        HikariDataSource dataSource = new HikariDataSource();
        if ("default".equals(key)) {
             Properties properties = PropertiesLoaderUtils.loadProperties(new EncodedResource(new ClassPathResource("application.properties"), "UTF-8"));
            dataSource.setJdbcUrl(properties.getProperty("spring.datasource.test1.jdbc-url"));
            dataSource.setUsername(properties.getProperty("spring.datasource.test1.username"));
            dataSource.setPassword(properties.getProperty("spring.datasource.test1.password"));
            dataSource.setDriverClassName(properties.getProperty("spring.datasource.test1.driver-class-name"));
        } else {
            //查询数据库
            ThreadLocalDataSource.setLocalSource("default");
            Databaseinfo dataBaseInfo = dataBaseService.getDataBaseInfo(key);
            dataSource.setJdbcUrl(dataBaseInfo.getUrl());
            dataSource.setUsername(dataBaseInfo.getUserName());
            dataSource.setPassword(dataBaseInfo.getPassword());
            dataSource.setDriverClassName(dataBaseInfo.getDriverClassName());
            DataSourceCache.put(key, dataSource);
            dynamicChangeDataSourceConfig.setTargetDataSources(DataSourceCache);
            dynamicChangeDataSourceConfig.afterPropertiesSet();
            ThreadLocalDataSource.setLocalSource(key);
        }
        return dataSource;
    }
}

当进入切点方法后,获取到前台传输的数据源key,去缓存中取,如果取不到,就查询数据库,并实例化放置到缓存中,并设置ThreadLocal的key为前台传输的key

4.总结

其实两种方案本质上是一种方法,第一种方法相当于自己把AbstractRoutingDataSource这个类的功能再手动的实现一遍,好处是更加灵活,可以针对自己的业务做定制