SpringBoot多数据源动态切换

简介: 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);

       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这个类的功能再手动的实现一遍,好处是更加灵活,可以针对自己的业务做定制

目录
相关文章
|
24天前
|
Java 调度 Spring
SpringBoot实现多线程定时任务动态定时任务配置文件配置定时任务
SpringBoot实现多线程定时任务动态定时任务配置文件配置定时任务
44 0
|
4月前
|
Java 开发工具 git
spring boot 集成 ctrip apollo 实现动态配置更新
spring boot 集成 ctrip apollo 实现动态配置更新
50 1
|
4月前
|
前端开发 Java 数据库连接
Spring Boot 3 整合 Mybatis-Plus 动态数据源实现多数据源切换
Spring Boot 3 整合 Mybatis-Plus 动态数据源实现多数据源切换
|
17天前
|
安全 数据安全/隐私保护
Springboot+Spring security +jwt认证+动态授权
Springboot+Spring security +jwt认证+动态授权
|
16天前
|
存储 关系型数据库 MySQL
【mybatis-plus】Springboot+AOP+自定义注解实现多数据源操作(数据源信息存在数据库)
【mybatis-plus】Springboot+AOP+自定义注解实现多数据源操作(数据源信息存在数据库)
|
6月前
|
数据挖掘 Java 测试技术
无代码动态表单系统 毕业设计 JAVA+Vue+SpringBoot+MySQL(一)
无代码动态表单系统 毕业设计 JAVA+Vue+SpringBoot+MySQL
|
2月前
|
存储 Java 关系型数据库
springboot整合多数据源的配置以及动态切换数据源,注解切换数据源
springboot整合多数据源的配置以及动态切换数据源,注解切换数据源
71 0
QGS
|
3月前
|
Java 关系型数据库 MySQL
手拉手springboot3整合mybatis-plus多数据源
手拉手springboot3整合mybatis-plus多数据源
QGS
69 1
|
3月前
|
Java 数据库 数据安全/隐私保护
使用Spring Boot和JPA实现多数据源的方法
使用Spring Boot和JPA实现多数据源的方法
51 0
|
3月前
|
Java 数据库连接 数据库
【Spring技术专题】「实战开发系列」保姆级教你SpringBoot整合Mybatis框架实现多数据源的静态数据源和动态数据源配置落地
Mybatis是一个基于JDBC实现的,支持普通 SQL 查询、存储过程和高级映射的优秀持久层框架,去掉了几乎所有的 JDBC 代码和参数的手工设置以及对结果集的检索封装。 Mybatis主要思想是将程序中大量的 SQL 语句剥离出来,配置在配置文件中,以实现 SQL 的灵活配置。在所有 ORM 框架中都有一个非常重要的媒介——PO(持久化对象),PO 的作用就是完成持久化操作,通过该对象对数据库执行增删改的操作,以面向对象的方式操作数据库。
45 1
【Spring技术专题】「实战开发系列」保姆级教你SpringBoot整合Mybatis框架实现多数据源的静态数据源和动态数据源配置落地