动态数据源切换实现方案
解决方案:
- 方案一:配置文件配置多数据源,如默认数据源:master,数据源1:salve1…,运行时动态切换已配置的数据源(master、salve1互相切换),无法在运行时动态添加配置文件中未配置的数据源。
- 方案二:配置一个默认数据源,运行时动态添加新数据源使用(适用于此场景)
执行流程:
具体实现步骤:
Spring提供了AbstractRoutingDataSource用于动态路由数据源,我们需要覆写protected DataSource determineTargetDataSource方法。
package com.hitotek.databasedemo.config.data.source;
import com.hitotek.databasedemo.utils.SpringUtils;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* <p>使用步骤:</p>
* <blockquote><pre>
* DynamicDataSource.dataSourcesMap.put(dataSourceKey, druidDataSource);
* DynamicDataSource.setDataSource(dataSourceKey);
* 调用业务代码</i>
* DynamicDataSource.clear();
* </pre></blockquote>
* @author : xphu
* @date : 2022-01-13 19:57
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
private static final ThreadLocal<String> dataSourceKey = ThreadLocal.withInitial(() -> "defaultDataSource");
public static Map<Object, Object> dataSourcesMap = new ConcurrentHashMap<>(10);
static {
dataSourcesMap.put("defaultDataSource", SpringUtils.getBean("defaultDataSource"));
}
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSource.dataSourceKey.get();
}
public static void setDataSource(String dataSource) {
DynamicDataSource.dataSourceKey.set(dataSource);
DynamicDataSource dynamicDataSource = (DynamicDataSource) SpringUtils.getBean("dataSource");
dynamicDataSource.afterPropertiesSet();
}
public static String getDataSource() {
return DynamicDataSource.dataSourceKey.get();
}
public static void clear() {
DynamicDataSource.dataSourceKey.remove();
}
}
然后需要配置默认的数据源:defaultDataSource
package com.hitotek.databasedemo.config.data.source;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Primary;
import javax.sql.DataSource;
/**
* Created with IntelliJ IDEA.
*
* @author : xphu
* @date : 2022-01-13 20:01
*/
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.druid")
public DataSource defaultDataSource() {
return DruidDataSourceBuilder.create().build();
}
@Bean
@Primary
@DependsOn({"springUtils", "defaultDataSource"})
public DynamicDataSource dataSource() {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setTargetDataSources(DynamicDataSource.dataSourcesMap);
return dynamicDataSource;
}
}
SpringUtils工具:
package com.hitotek.databasedemo.utils;
import lombok.Getter;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
* Created with IntelliJ IDEA.
*
* @author : xphu
* @date : 2022-01-13 19:58
*/
@Component
public final class SpringUtils implements ApplicationContextAware {
@Getter
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if (SpringUtils.applicationContext == null) {
SpringUtils.applicationContext = applicationContext;
}
}
public static <T> T getBean(Class<T> clazz) {
return SpringUtils.applicationContext.getBean(clazz);
}
public static Object getBean(String name) {
return SpringUtils.applicationContext.getBean(name);
}
public static String getProperty(String key) {
return SpringUtils.applicationContext.getEnvironment().getProperty(key);
}
}
使用时直接调用DynamicDataSource.setDataSource(DataSource dataSource)方法即可,使用完后调用DynamicDataSource.clear()防止内存泄漏并重置默认数据源。
使用方法:
- 切换已经加载的数据源
// 切换数据源 ‘数据源名称’
DynamicDataSource.setDataSource("dbkey");
// ... 执行业务代码
// 恢复默认数据源
DynamicDataSource.clear();
- 创建新的数据源并切换
// 创建数据源对象
DruidDataSource druidDataSource = new DruidDataSource();
// 填写数据连接地址
druidDataSource.setUrl("jdbc:mysql://localhost:3306/sys?characterEncoding=utf-8&useSSL=false&serverTimezone=UTC&useAffectedRows=true");
// 用户名和密码
druidDataSource.setUsername("root");
druidDataSource.setPassword("root");
// 数据库驱动包
druidDataSource.setDriverClassName(dto.getDriverClassName());
// 数据库类型
druidDataSource.setDbType(DbType.valueOf(dto.getType()));
DynamicDataSource.dataSourcesMap.put("dbkey", druidDataSource);
DynamicDataSource.setDataSource("dbkey");
//此时数据源已切换到druidDataSource ,调用自己的业务方法即可。
//使用完后调用DynamicDataSource.clear();重置为默认数据源。
DynamicDataSource.clear();
数据库设计
database_type:数据库类型表(字典表,不太能用户管理,里面的驱动包和模板修错了,系统就运行不了了)
字段 | 类型 | 备注 | 案例 |
---|---|---|---|
id | int | 主键 | 1 |
name | varchar | 数据库名称 | MySQL |
type | varchar | 数据库类型:sqlserver、postgresql、mysql、oracle | mysql |
driver_class_name | varchar | 驱动包(必须项目里面引用了cdk,才可以用) | com.mysql.cj.jdbc.Driver |
url_template | varchar | 数据库地址模板 | jdbc:mysql://%s:%s/%s?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8 |
data_source:数据源表
字段 | 类型 | 备注 | 案例 |
---|---|---|---|
id | int | 主键 | 1 |
data_source_name | varchar | 数据源名称 | postgresql_smartpark_ep |
host | varchar | host地址 | 11.4.2.53 |
port | varchar | 端口号 | 5432 |
username | varchar | 数据库账号 | liantu |
password | varchar | 数据库密码 | 121212 |
db_name | varchar | 数据库名称 | smartpark |
db_type | varchar | 数据库类型 | postgresql |
driver_class_name | varchar | 驱动包 | org.postgresql.Driver |
schema_name | varchar | 模式 | ep |
url_template | varchar | 数据库地址模板 | jdbc:postgresql://%s:%s/%s?currentSchema=%s&useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8 |
create_time | datetime | 添加时间 | 2022-01-13 13:05:06 |
update_time | datetime | 修改时间 | 2022-01-13 13:05:06 |
mark | varchar | 标识 | |
description | varchar | 描述 | 我是PG数据库 |
接口设计
查询数据源列表
接口地址:/dataSource
请求方式:GET
接口描述: 查询数据源列表
请求参数:
参数名称 | 参数说明 | in | 是否必须 | 数据类型 |
---|---|---|---|---|
current | current | query | false | integer(int32) |
size | size | query | false | integer(int32) |
响应示例:
{
"code": 200,
"message": "接口调用成功",
"dateTime": "2022-01-16 13:11:45",
"serialVersionUID": 8545996863226528798,
"records": [
{
"id": 12,
"pollName": "base_project",
"type": "mysql",
"schemaName": null,
"databaseName": "base_project",
"driverClassName": "com.mysql.cj.jdbc.Driver",
"url": "apxxxto.cn",
"port": "3306x",
"username": "root",
"password": "xxx",
"mark": null,
"urlTemplate": "jdbc:mysql://%s:%s/%s?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8",
"createTime": "2022-01-13T09:30:20.000+0000",
"updateTime": "2022-01-14T16:14:32.000+0000"
},
{
"id": 14,
"pollName": "postgresql_smartpark_ep",
"type": "postgresql",
"schemaName": "ep",
"databaseName": "smartpark",
"driverClassName": "org.postgresql.Driver",
"url": "11.4.12.53",
"port": "5432",
"username": "liantu",
"password": "1111",
"mark": null,
"urlTemplate": "jdbc:postgresql://%s:%s/%s?currentSchema=%s&useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8",
"createTime": "2022-01-13T13:05:06.000+0000",
"updateTime": "2022-01-14T16:14:34.000+0000"
},
{
"id": 15,
"pollName": "sqlserver_ltep3",
"type": "sqlserver",
"schemaName": null,
"databaseName": "ltep3",
"driverClassName": "com.microsoft.sqlserver.jdbc.SQLServerDriver",
"url": "102.4.1.22",
"port": "433",
"username": "ltep",
"password": "ltep123",
"mark": null,
"urlTemplate": "jdbc:sqlserver://%s:%s;DatabaseName=%s",
"createTime": "2022-01-14T07:09:48.000+0000",
"updateTime": "2022-01-14T16:14:36.000+0000"
}
],
"total": 0,
"size": 10,
"current": 1,
"orders": [],
"optimizeCountSql": true,
"isSearchCount": true
}
查询当前数据源
接口地址:/dataSource/now
请求方式:GET
接口描述: 查询数据源列表
请求参数:
响应示例:
{
"code":200,
"message":"接口调用成功",
"dateTime":"2022-01-15 21:38:29",
"data":"defaultDataSource"
}
添加数据源
接口地址:/dataSource
请求方式:POST
请求数据类型:application/json
接口描述: 添加数据源
请求参数:
参数名称 | 参数说明 | 是否必须 | 数据类型 |
---|---|---|---|
id | 主键 | false | integer(int32) |
pollName | 数据源名称 | true | string |
type | 数据库类型 | true | string |
schemaName | 模式 | true | string |
databaseName | 数据库名称 | true | string |
driverClassName | 驱动包 | true | string |
url | 数据库地址 | true | string |
port | 数据库端口 | true | string |
username | 用户名 | true | string |
password | 密码 | true | string |
mark | 标识符 | false | string |
createTime | 创建时间 | false | string(date-time) |
updateTime | 修改时间 | false | string(date-time) |
响应示例:
{
"code": 200,
"message": "接口调用成功",
"dateTime": "2022-01-15 21:32:55",
"data": true
}
修改数据源
请求方式:PATCH
删除数据源
请求方式:DELETE
接口地址:/dataSource/{name}
接口描述: 根据数据源名称删除数据源
请求参数:
参数名称 | 参数说明 | 是否必须 | 数据类型 |
---|---|---|---|
name | 数据源名称 | true | string |
响应示例:
{
"code": 200,
"message": "接口调用成功",
"dateTime": "2022-01-15 21:34:16",
"data": true
}
查询已经加载的数据源
请求方式:GET
接口地址:/dataSource/now/list
接口描述: 查询已经加载的数据源
请求参数: 无
响应示例:
{
"code": 200,
"message": "接口调用成功",
"dateTime": "2022-01-15 21:37:10",
"data": [
"ltep3",
"smartpark",
"defaultDataSource",
"base_project"
],
"size": 4
}
查询数据源列表
接口地址:/dataSource
请求方式:GET
接口描述: 查询数据源列表
请求参数:
参数名称 | 参数说明 | 是否必须 | 数据类型 |
---|---|---|---|
current | current | false | integer(int32) |
size | size | false | integer(int32) |
响应示例:
{
"current": 0,
"hitCount": true,
"pages": 0,
"records": [
{
"id": 0,
"pollName": "数据源名称",
"databaseName": "数据库名",
"port": "端口",
"type": "数据库类型",
"url": "ip地址",
"username": "用户名",
"password": "密码",
"driverClassName": "驱动包",
"schemaName": "模式(部分数据库拥有)",
"mark": "标识符",
"url_template": '数据库地址模板',
"updateTime": "修改时间",
"createTime": "创建时间"
}
],
"searchCount": true,
"size": 0,
"total": 0
}