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);
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這個類的功能再手動的實現一遍,好處是更加靈活,可以針對自己的業務做定制

原文鏈接:https://blog.csdn.net/white_bird_shit/article/details/109496861


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM