springboot实现动态数据源访问多个数据库
1. 简介和实现类介绍
昨天进行开发的时候,遇到一个问题,在不同的数据指标下,访问的数据库是不一样的,但是又在同一个数据库链接里面,以前用到的都是单数据源,数据库不会变,第一次遇到这个问题,网上查了一下资料,网上很多都是多数据源的实现方式,无非就是写死的动态源,然后来回切换,但是在项目中我们不知道有几个动态源是不是就没办法写,难道还需要来一个数据源我们就要加一套数据源的配置,比较麻烦,所以只能做成动态的,实现最小化的改动来实现多数据源,所以我们把数据源存在了,yml文件里面最终用map读,实现动态多数据源的问题。
实现类名 | 实现类的用途 |
---|---|
ClearIdleTimerTask | 清除空闲连接任务 |
DataSourceByMapNameConfig | 多数据源的存储 |
DataSourceHolder | 动态数据源管理器 |
DataSourceTimer | 动态数据源定时器管理。长时间无访问的数据库连接关闭 |
DateSourceConfig | 数据源配置管理 |
DynamicDataSource | 定义动态数据源派生类。从基础的DataSource派生,动态性自己实现 |
DBIdentifier | 数据库标识管理类。用于区分数据源连接的不同数据库。 |
大致就是这个样子的几个类来实现的,下面进行代码详解
哦!对了,还要依赖一个jar包,maven地址在这
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
<version>8.5.21</version>
</dependency>
2. 项目创建实现类
2.1添加数据源配置 DateSourceConfig
package com.casic.collect.config;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
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 javax.sql.DataSource;
/**
* 数据源配置管理。
*
* @author jiangs
* @version 2020年12月17日
*/
@Configuration
@MapperScan(basePackages="com.test.collect.mapper", value="sqlSessionFactory")
public class DateSourceConfig {
/**
* 根据配置参数创建数据源。使用派生的子类。
* @return 数据源
*/
@Bean(name="dataSource")
@ConfigurationProperties(prefix="spring.datasource")
public DataSource getDataSource() {
DataSourceBuilder builder = DataSourceBuilder.create();
builder.type(DynamicDataSource.class);
return builder.build();
}
/**
* 创建会话工厂。
*
* @param dataSource 数据源
* @return 会话工厂
*/
@Bean(name="sqlSessionFactory")
public SqlSessionFactory getSqlSessionFactory(@Qualifier("dataSource") DataSource dataSource) {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
try {
return bean.getObject();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
2.2.定义动态数据源
2.2.1 数据库标识管理类 DBIdentifier
这里我是直接通过yml获取的map所以,请看后面的yml获取键值对。
首先增加一个数据库标识类,用于区分不同的数据库访问。
由于我们为不同的project创建了单独的数据库,所以使用项目编码作为数据库的索引。而微服务支持多线程并发的,采用线程变量。
/**
* 数据库标识管理类。用于区分数据源连接的不同数据库。
*
* @author jiangs
* @version 2020-12-17
*/
public class DBIdentifier {
/**
* 用不同的工程编码来区分数据库
*/
private static ThreadLocal<String> projectCode = new ThreadLocal<String>();
private static Map<String,String> dbName = Maps.newHashMap();
public static Map<String, String> getMap() {
return dbName;
}
public static String getProjectCode() {
return projectCode.get();
}
public static void setProjectCode(String code,Map map) {
projectCode.set(code);
dbName = map;
}
}
2.2.2.定义动态数据源派生类 DynamicDataSource
从DataSource派生了一个DynamicDataSource,在其中实现数据库连接的动态切换
/**
* 定义动态数据源派生类。从基础的DataSource派生,动态性自己实现。
*
* @author jiangs
* @version 2020-12-17
*/
public class DynamicDataSource extends DataSource {
private static Logger log = LogManager.getLogger(DynamicDataSource.class);
/**
* 改写本方法是为了在请求不同工程的数据时去连接不同的数据库。
*/
@Override
public Connection getConnection(){
String projectCode = DBIdentifier.getProjectCode();
//1、获取数据源
DataSource dataSource = DataSourceHolder.instance().getDataSource(projectCode);
//2、如果数据源不存在则创建
if (dataSource == null) {
try {
DataSource initDataSource = initDataSource(projectCode);
DataSourceHolder.instance().addDataSource(projectCode, initDataSource);
} catch (IllegalArgumentException | IllegalAccessException e) {
log.error("Init data source fail. projectCode:" + projectCode);
return null;
}
}
dataSource = DataSourceHolder.instance().getDataSource(projectCode);
try {
return dataSource.getConnection();
} catch (SQLException e) {
e.printStackTrace();
return null;
}
}
/**
* 以当前数据对象作为模板复制一份。
*
* @return DataSource
* @throws IllegalAccessException
* @throws IllegalArgumentException
*/
private DataSource initDataSource(String projectCode) throws IllegalArgumentException, IllegalAccessException {
DataSource dataSource = new DataSource();
// 2、复制PoolConfiguration的属性
PoolProperties property = new PoolProperties();
Field[] fields = PoolProperties.class.getDeclaredFields();
for (Field f : fields) {
f.setAccessible(true);
Object value = f.get(this.getPoolProperties());
try{
f.set(property, value);
}
catch (Exception e){
log.info("Set value fail. attr name:" + f.getName());
continue;
}
}
dataSource.setPoolProperties(property);
// 3、设置数据库名称和IP(一般来说,端口和用户名、密码都是统一固定的)
String urlFormat = this.getUrl();
String url = String.format(urlFormat, DBIdentifier.getMap().get(projectCode));
dataSource.setUrl(url);
return dataSource;
}
}
2.2.3.动态数据源定时器管理 DataSourceTimer
动态数据源定时器管理。长时间无访问的数据库连接关闭。
/**
* 动态数据源定时器管理。长时间无访问的数据库连接关闭。
*
* @author jiangs
* @version 2020年12月17日
*/
public class DataSourceTimer {
/**
* 空闲时间周期。超过这个时长没有访问的数据库连接将被释放。默认为10分钟。
*/
private static long idlePeriodTime = 10 * 60 * 1000;
/**
* 动态数据源
*/
private DataSource dataSource;
/**
* 上一次访问的时间
*/
private long lastUseTime;
public DataSourceTimer(DataSource dataSource) {
this.dataSource = dataSource;
this.lastUseTime = System.currentTimeMillis();
}
/**
* 更新最近访问时间
*/
public void refreshTime() {
lastUseTime = System.currentTimeMillis();
}
/**
* 检测数据连接是否超时关闭。
*
* @return true-已超时关闭; false-未超时
*/
public boolean checkAndClose() {
if (System.currentTimeMillis() - lastUseTime > idlePeriodTime)
{
dataSource.close();
return true;
}
return false;
}
public DataSource getDataSource() {
return dataSource;
}
}
2.2.4.动态数据源管理器 DataSourceHolder
DataSourceHolder来管理不同的数据源,提供数据源的添加、查询功能。
里面添加了读写锁,并创建了ConcurrentHashMap来保证线程安全。
/**
* 动态数据源管理器。
*
* @author jiangs
* @version 2020年12月17日
*/
public class DataSourceHolder {
/**
* 管理动态数据源列表。<工程编码,数据源>
*/
private Map<String, DataSourceTimer> dataSourceTimerMap = new ConcurrentHashMap<>();
/**
* 通过定时任务周期性清除不使用的数据源
*/
private static Timer clearIdleTask = new Timer();
static {
clearIdleTask.schedule(new ClearIdleTimerTask(), 5000, 60 * 1000);
};
private DataSourceHolder() {
}
/**
* 单例构件类
* @author jiangs
* @version 2020年12月17日
*/
private static class DataSourceHolderBuilder {
private static DataSourceHolder instance = new DataSourceHolder();
}
/*
* 获取单例对象
*/
public static DataSourceHolder instance() {
return DataSourceHolderBuilder.instance;
}
/**
* 添加动态数据源。
*
* @param projectCode 项目编码
* @param dataSource 数据源
*/
public void addDataSource(String projectCode, DataSource dataSource) {
synchronized(dataSourceTimerMap) {
DataSourceTimer dataSourceTimer = new DataSourceTimer(dataSource);
dataSourceTimerMap.put(projectCode, dataSourceTimer);
};
}
/**
* 查询动态数据源
*
* @param projectCode 项目编码
* @return dds
*/
public DataSource getDataSource(String projectCode) {
synchronized(dataSourceTimerMap){
if (dataSourceTimerMap.containsKey(projectCode)) {
DataSourceTimer dataSourceTimer = dataSourceTimerMap.get(projectCode);
dataSourceTimer.refreshTime();
return dataSourceTimer.getDataSource();
}
};
return null;
}
/**
* 清除超时无人使用的数据源。
* 存在正在运行数据源会被清理掉风险!!!
*/
public synchronized void clearIdleDataSource() {
Iterator<Map.Entry<String, DataSourceTimer>> iter = dataSourceTimerMap.entrySet().iterator();
for (; iter.hasNext(); ) {
Map.Entry<String, DataSourceTimer> entry = iter.next();
if (entry.getValue().checkAndClose())
{
iter.remove();
}
}
}
}
2.2.5.定时器任务,用于定时清除空闲的数据源 ClearIdleTimerTask
/**
* 清除空闲连接任务。
*
* @author jiangs
* @version 2020年12月17日
*/
public class ClearIdleTimerTask extends TimerTask {
@Override
public void run() {
DataSourceHolder.instance().clearIdleDataSource();
}
}
2.3.管理项目编码名称的映射关系DataSourceByMapNameConfig
先上代码,这个放在第三节来解释
@Component
@ConfigurationProperties(prefix = "data")
@Data
public class DataSourceByMapNameConfig {
private Map<String,String> map = Maps.newHashMap();
private String name;
public Map getMap() {
return map;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
这是yml的数据源配置,需要动态的地方用通配符%s进行替换
然后在DynamicDataSource 这个类的
进行通配符替换,有几个通配符里面就写几个参数,实在不懂的可以留言告诉我,秒回复的哦~
3. spring boot 读取yml中的map即键值对
这章节对于这个yml获取来进行详解,首先,在yml文件里面写上自己的动态数据
data是定义名字,在config的注解里面会进行匹配,map是集合的key值,下面是集合的value值,因为我的项目需要所以我得value值是一个map,也可以根据自己的需求来添加。
注意:
key和value值中间隔得是空格而不是Tab,就因为这个今天折磨我了两个小时。
上面代码里面的注解@Data是lombok的一个插件,可以自行下载这里不详解,不需要可不加。
@ConfigurationProperties这个注解里面的参数对应的是yml的data。
我的调用是在controller里面直接调用传值进动态数据源的
4. 总结
这个鬼东西吧,说难也难,说简单也简单,就是看个人对底层的理解,反正我是觉得挺难得,由于时间的关系,我写的很仓促,又看不懂的可以私下问我,留言什么的都可以,主要是秒回!!!,谢谢大家的观看,每天做好笔记是对自己提升的重要阶段,写这个也是为了以后查看方便和对自己的理解加深。😁
参考文档:https://www.jb51.net/article/135688.htm