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

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值