返回
Featured image of post 动态数据源切换实现方案

动态数据源切换实现方案

动态数据源切换实现方案

解决方案:

  • 方案一:配置文件配置多数据源,如默认数据源:master,数据源1:salve1…,运行时动态切换已配置的数据源(master、salve1互相切换),无法在运行时动态添加配置文件中未配置的数据源。
  • 方案二:配置一个默认数据源,运行时动态添加新数据源使用(适用于此场景)

执行流程:

image-20220114160205520
image-20220114160205520

具体实现步骤:

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()防止内存泄漏并重置默认数据源。

使用方法:

  1. 切换已经加载的数据源
// 切换数据源 ‘数据源名称’
DynamicDataSource.setDataSource("dbkey");
// ... 执行业务代码
// 恢复默认数据源
DynamicDataSource.clear();	
  1. 创建新的数据源并切换
    // 创建数据源对象
	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
}