手撕数据库连接池

news2024/11/20 14:27:59

1.有开源的数据库连接池,你为啥不用?

这个不是因为闲的没事干,先说下需求背景 

我们有一个数据源管理模块,配置的数据源连接,用户名,密码等信息

在数据源管理模块配置好之后,去另一个模块选择数据源,获取每个数据源下库表结构以及字段

之前是每次都会创建一个新的连接,那这肯定会比较慢,反复打开和关闭连接,耗费时间

但是我们平时用的Druid连接池,我研究了一下,似乎没办法我说的这种业务

1.因为Druid虽然支持多数据源,但一般都是支持两个,三个数据源进行切换。

但是我们业务可能达到上百个数据源连接

2.而且Druid是需要在yml文件中,提前配置好你需要哪几个数据库,把连接信息写上

我们可能自由的修改新增删除,无法在项目启动的时候就知道我有哪些数据源需要操作

2.思路

 这种应该是有开源框架的,但是我没找到,就花了一天时间写了一个

1.项目启动的时候,我会加载数据源模块的数据,

以jdbcurl+username为key

用concurrentHashMap为每个数据源创建一个连接  

2.实体结构

2.1先创建一个实体ConnEntity,里边仨属性,一个conn,一个当前conn被几个人使用,一个现在conn是否在被使用状态

 2.2再创建一个实体ConnCacheEntity,这个就是连接池Map的value

里边俩属性 一个是connList,一个是当前访问数

当前访问数是说当前jdbcurl+username为key的情况下有多少个人使用

2.3连接池map

concurrentHashMap

key=jdbcurl+username

value=ConnCacheEntity

 3.思想

连接池就是为了连接可以复用,不需要每次都去创建一个新的连接,在释放的时候不是真正的关闭连接,而是把链接还给池子

在知道连接池思想的前提下,再去说手撕数据库连接池这件事

 4.获得连接

* 通过url username 从内存中获取连接 或者新建连接 放到内存中并且返回
* 通过key 取连接信息 没有的话就新建立一个连接返回,有的话就当前使用数最少的那个返回
* 当前存在可用连接  算出目前访问数和已有连接的比例  <=标准比例 则返回一个现在使用数量最小的连接  >标准比例  创建一个连接  与第一个步骤相同
* 我现在的设计是一个conn可以同时被五个人访问,所以现在如果发现有6个人 那就要新创建一个连接返回了
* 而且不管是返回哪个连接,都要设置这个连接被使用标识为true,被使用次数+1,而且key对应的使用人数+1
* 使用连接之后 需要在finally方法中把当前连接数-1 如果当前连接数-1之后为0  则 当前连接使用标识为false
* 使用连接之后 还要根据url+username组成的key 把当前对应的访问数-1

5.释放连接

释放连接中有些操作就是和获得连接时候相反了

* 如果当前连接被使用的人数为0 则使用标识设置为false
* 同数据库连接池 酌情关闭连接
* 因为五个人用一个连接  所以 人数/连接数=0.2 是目前标准
* 如果目前  人数*0.2=目前应有的连接数
* 如果实际连接数>目前应有连接数 就把目前没有被用到的连接关闭,并且移除数组
* 连接数至少要留一个
* 如果实际连接数>目前应有连接数 算出差了几个 就把目前没有被用到的连接关闭,并且移除数组
* 当前是把连接还给池子  所以池子的访问数量-1
* 当前conn的连接数量-1

6.注意点

mysql 连接空闲8小时会自动断开连接 所以返回连接之前需要检测 如果当前连接关闭 就再建一个连接返回

  发现这一点之后,我不光修改了getConn方法,还加了个定时,每天八点,看哪个连接关了,就新建一个连接

3.项目启动时候初始化连接池

@Component
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
@RequiredArgsConstructor
public class CommandLineRunnerImpl implements CommandLineRunner {

    SysDataSourceService sysDataSourceService;
    @Override
    public void run(String... args){
        this.createDBConn();
    }

    /**
     * 程序启动时,对于数据源表中 success状态的 建立连接
     */
    private void createDBConn(){
        List<SysDataSource> list = sysDataSourceService.list(new LambdaQueryWrapper<SysDataSource>().eq(SysDataSource::getTestStatus, LinkTestStatus.SUCCESS.name()));
        if(CollectionUtils.isEmpty(list)){
            return;
        }
        list.forEach(source -> ConnFactory.getConn(source.getLinkInfo(),source.getUsername(),source.getPassword()));
    }

}

4.缓存实体

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ConnCacheEntity {

    List<ConnEntity> connEntityList;

    /**
     * 当前访问数
     * key为url+username
     * 记录当前url+username下的访问数
     * 每次get时 +1 用完 -1
     */
    int linkCount;

    /**
     * url username password用于连接关闭的情况下  再次建立连接时使用
     */
    String url;

    String user;

    String password;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ConnEntity {

    /**
     * 连接对象
     */
    Connection conn;

    /**
     * 当前连接是否正在被使用
     * 每次getConn时 给当前使用对象赋值为true  使用之后 赋值为false
     */
    boolean use;

    /**
     * 使用数量
     * 每次getConn时 +1  使用之后 -1
     */
    int useCount;

    /**
     * 用于连接测试时展示具体失败原因
     */
    SQLException e;
}

5.创建和销毁时公用属性

    /**
     * key是url_username
     * value是对应的连接信息
     * 相当于数据库连接池
     */
    public static Map<String, ConnCacheEntity> connMap = new ConcurrentHashMap<>();

    /**
     * 访问数与现在内存中已经建立的数据库连接数的比例
     * 5个人 用一个连接
     */
    private static final BigDecimal LINK_DIVIDE_CONN = BigDecimal.valueOf(5);

    /**
     * 5个人用一个连接 1:5=0.2
     */
    private static final BigDecimal CONN_DIVIDE_LINK = BigDecimal.valueOf(0.2);
    /**
     * 每个数据源,最大能打开的连接数
     */
    private static final int MAX_LINK = 10;

    /**
     * key的组成方式 url_username
     *
     * @param url
     * @param user
     * @return
     */
    private static String getKey(String url, String user) {
        return url.concat(Constant.UNDERLINE).concat(user);
    }

6.创建连接

    /**
     * 获得数据库连接
     * 通过url username 从内存中获取连接 或者新建连接 放到内存中并且返回
     * 使用连接之后 需要在finally方法中把当前连接数-1 如果当前连接数-1之后为0  则 当前连接使用标识为false
     * 使用连接之后 还要根据url+username组成的key 把当前对应的访问数-1
     *
     * @param url
     * @param user
     * @param password
     * @return
     */
    public static ConnEntity getConn(String url, String user, String password) {
        if (StringUtils.isEmpty(url) || StringUtils.isEmpty(user)) {
            SQLException sqlException = new SQLException("连接信息有误,请检查用户名端口号等信息");
            return ConnEntity.builder().e(sqlException).build();
        }
        //拼接Key
        String key = getKey(url, user);
        //通过key 取连接信息 没有的话就新建立一个连接返回,有的话就当前使用数最少的那个返回
        ConnCacheEntity connCacheEntity = connMap.get(key);
        if (Objects.isNull(connCacheEntity) || CollectionUtils.isEmpty(connCacheEntity.getConnEntityList())) {
            //如果对象不为空但是对应的可用连接数为空
            try {
                Connection conn = DriverManager.getConnection(url, user, password);
                //因为新建的 即将返回  所以 当前连接正在使用 使用数量为1
                ConnEntity connEntity = ConnEntity.builder().conn(conn).use(true).useCount(1).build();
                //因为新建的 设置当前访问数为1
                List<ConnEntity> connEntities = new ArrayList<>();
                connEntities.add(connEntity);
                connCacheEntity = ConnCacheEntity.builder().connEntityList(connEntities).linkCount(1).url(url).user(user).password(password).build();
                connMap.put(key, connCacheEntity);
                return connEntity;
            } catch (SQLException e) {
                log.error(e.getMessage());
                return ConnEntity.builder().e(e).build();
            }
        } else {
            //当前连接数+1
            connCacheEntity.setLinkCount(connCacheEntity.getLinkCount() + 1);
            //当前存在可用连接  算出目前访问数和已有连接的比例  <=标准比例 则返回一个现在使用数量最小的连接  >标准比例  创建一个连接  与第一个步骤相同
            //或者是当前连接已经达到上限 就不创建新的连接了 就把目前占用最少的那个conn返回
            int linkCount = connCacheEntity.getLinkCount();
            int size = connCacheEntity.getConnEntityList().size();
            BigDecimal divide = new BigDecimal(linkCount).divide(new BigDecimal(size));
            if (divide.compareTo(LINK_DIVIDE_CONN) <= 0 || size >= MAX_LINK) {
                connCacheEntity.getConnEntityList().sort(Comparator.comparing(ConnEntity::getUseCount));
                //正序 第一个是最小值 赋值 当前正在被使用 并且 使用数+1
                ConnEntity connEntity = connCacheEntity.getConnEntityList().get(0);
                //mysql 连接空闲8小时会自动断开连接 所以返回连接之前需要检测 如果当前连接关闭 就再建一个连接返回
                try {
                    if(connEntity.getConn().isClosed()){
                        Connection conn = DriverManager.getConnection(url, user, password);
                        connEntity.setConn(conn);
                    }
                } catch (SQLException e) {
                    throw new RuntimeException(e);
                }
                connEntity.setUse(true);
                connEntity.setUseCount(connEntity.getUseCount() + 1);
                return connEntity;
            } else {
                try {
                    Connection conn = DriverManager.getConnection(url, user, password);
                    //因为新建的 即将返回  所以 当前连接正在使用 使用数量为1
                    ConnEntity connEntity = ConnEntity.builder().conn(conn).use(true).useCount(1).build();
                    connCacheEntity.getConnEntityList().add(connEntity);
                    //新的连接加入到list中并返回
                    return connEntity;
                } catch (SQLException e) {
                    log.error(e.getMessage());
                    return ConnEntity.builder().e(e).build();
                }
            }
        }
    }

7.销毁连接

  /**
     * 关闭数据库连接
     * 如果当前连接被使用的人数为0 则使用标识设置为null
     * 同数据库连接池 酌情关闭连接
     * 因为五个人用一个连接  所以 人数/连接数=0.2 是目前标准
     * 如果目前  人数*0.2=目前应有的连接数
     * 如果实际连接数>目前应有连接数 就把目前没有被用到的连接关闭,并且移除数组
     *
     * @param connEntity
     * @param url
     * @param user
     */
    public static void  closeConn(ConnEntity connEntity, String url, String user) {
        if (Objects.isNull(connEntity) || StringUtils.isEmpty(url) || StringUtils.isEmpty(user)) {
            return;
        }
        //拼接Key
        String key = getKey(url, user);
        //当前是把连接还给池子  所以池子的访问数量-1
        ConnCacheEntity connCache = connMap.get(key);
        connCache.setLinkCount(connCache.getLinkCount() - 1);
        //当前conn的连接数量-1
        int useCount = connEntity.getUseCount();
        if (useCount > 0) {
            connEntity.setUseCount(connEntity.getUseCount() - 1);
        }
        //如果当前连接被使用的人数为0 则使用标识设置为null
        if (connEntity.getUseCount() == 0) {
            connEntity.setUse(false);
        }
        //人数*0.2=目前应有的连接数   如果实际连接数>目前应有连接数 就把目前没有被用到的连接关闭,并且移除数组
        BigDecimal multiply = new BigDecimal(connCache.getLinkCount()).multiply(CONN_DIVIDE_LINK);
        //向上取整  防止2*0.2=0.4<1 然后把最后一个连接关闭的这种情况发生
        int roundedNumber = multiply.setScale(0, RoundingMode.UP).intValue();
        //实际连接数
        int size = connCache.getConnEntityList().size();
        //连接数至少要留一个
        if (size > roundedNumber && size > 1) {
            //如果实际连接数>目前应有连接数 算出差了几个 就把目前没有被用到的连接关闭,并且移除数组
            int subtract = size - roundedNumber;
            List<ConnEntity> removes = new ArrayList<>();
            for (ConnEntity entity : connCache.getConnEntityList()) {
                if (!entity.isUse() && subtract > 0) {
                    try {
                        entity.getConn().close();
                        removes.add(entity);
                        subtract--;
                    } catch (SQLException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
            connCache.getConnEntityList().removeAll(removes);
        }
    }

8.使用方式

用我提供的静态方法获取conn

还是要在finally中去调用我提供的关闭连接的方法

但是不要自己去conn.close();而是调用静态方法由我来决定是否关闭

    private static List<ColumnBO> getColumnNames(ConnectQueryBO connectQueryBO) {
        // 调用方法并计时
        long startTime = System.currentTimeMillis();
        ConnEntity conn1 = null;
        List<ColumnBO> list = new ArrayList<>();
        Statement stmt = null;
        ResultSet resultSet = null;
        try {
            // conn = DriverManager.getConnection(connectQueryBO.getUrl(), connectQueryBO.getUser(), connectQueryBO.getPassword());
            conn1 = ConnFactory.getConn(connectQueryBO.getUrl(), connectQueryBO.getUser(), connectQueryBO.getPassword());
            Connection conn = conn1.getConn();
            stmt = conn.createStatement();
            resultSet = stmt.executeQuery(connectQueryBO.getQuerySql());
            //有dba权限情况下才会执行这部
            while (resultSet.next()) {//如果对象中有数据,就会循环打印出来
                String columName = resultSet.getString(COLUMN_NAME);
                String dataType = resultSet.getString(DATA_TYPE);
                list.add(ColumnBO.builder().columnName(columName).dataType(dataType).build());
            }
        } catch (SQLException e) {
            //select TABLE_NAME from all_tables WHERE owner="+dbName 这个语句可能没有权限执行
            log.error(e.getMessage());
        } finally {
            //finally中关闭连接
            ConnFactory.closeConn(conn1, connectQueryBO.getUrl(), connectQueryBO.getUser());
            try {
                if (Objects.nonNull(stmt)) {
                    stmt.close();
                }
                if (Objects.nonNull(resultSet)) {
                    resultSet.close();
                }
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
        // 计算执行时间
        long endTime = System.currentTimeMillis();
        long executionTime = endTime - startTime;
        // 打印执行时间
        System.out.println("getColumnNames方法执行时间:" + executionTime + "毫秒");
        return list.stream().distinct().collect(Collectors.toList());
    }

9.定时检验已关闭的连接

这个是踩了坑才想起来加的

No operations allowed after connection closed

mysql如果连接空闲了8小时,会自动关闭

@Configuration
@EnableScheduling
public class ConnCheck {

    /**
     * 每天早上八点,把已经关闭的连接,重新打开
     * 因为mysql的数据库连接空闲8小时会自动断开
     * 在这里提前重新打开,可以加快效率
     */
    @Scheduled(cron = "0 0 8 * * ?")
    public void check() {
        for (Map.Entry<String, ConnCacheEntity> entry : ConnFactory.connMap.entrySet()) {
            ConnCacheEntity entity = entry.getValue();
            if (Objects.isNull(entity)) {
                continue;
            }
            List<ConnEntity> connEntityList = entity.getConnEntityList();
            if (CollectionUtils.isEmpty(connEntityList)) {
                continue;
            }
            connEntityList.forEach(connEntity -> {
                try {
                    boolean closed = connEntity.getConn().isClosed();
                    //如果连接已经关闭 就重新打开
                    if (closed) {
                        Connection conn = DriverManager.getConnection(entity.getUrl(), entity.getUser(), entity.getPassword());
                        connEntity.setConn(conn);
                    }
                } catch (SQLException e) {
                    throw new RuntimeException(e);
                }
            });
        }
    }

}

10.UnsupportedOperationException

 这个是踩了个坑

Collections.singletonList和Arrays.asList()和正常new 出来的List使用方式有所不同

我开始是新建conn时 就Collections.singletonList了

后来销毁连接时候  removeAll出错了

报了个UnsupportedOperationException

所以我改成了这样

 Collections.singletonList()返回的是不可变的集合,但是这个长度的集合只有1,可以减少内存空间。但是返回的值依然是Collections的内部实现类,同样没有add的方法,调用add,set方法会报错

Arrays.asList返回的集合不支持元素的添加和删除。也就是不可以使用add、addAll和remove操作。

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1199451.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

日语形容词分类

变成肯定形式很简单&#xff0c;就是在后面加个です就可以了&#xff0c;不列在表格了 一类形容词 汉字い并且是以い结尾的形容词 否定形式是去掉い然后在后面加上くないです 更加正式的否定是去掉い然后加上くありません 日文平假名过去式过去否定中文熱いあつい熱くないで…

[护网杯 2018]easy_tornado 1(两种解法!)

题目环境&#xff1a;发现有三个txt文本文件 /flag.txt/welcome.txt/hints.txt 依此点开 flag在/fllllllllllllag文件中 在hints.txt文件中发现md5计算 md5(cookie_secretmd5(filename)) 并且三个文件中都存在filehash&#xff08;文件名被哈希算法加密32位小写&#xff09; 猜…

Halcon WPF 开发学习笔记(2):Halcon导出c#脚本和WPF初步开发

文章目录 前言HalconC#教学简单说明如何二开机器视觉如何二次开发Halcon导出Halcon脚本新建WPF项目&#xff0c;导入Halcon脚本和Halcon命名空间 前言 我目前搜了一下我了解的机器视觉软件&#xff0c;有如下特点 优点缺点兼容性教学视频(B站前三播放量)OpenCV开源&#xff0…

如何让VirtualBox系统使用Ubuntu主机的USB

如何让VirtualBox系统使用Ubuntu主机的USB 当通过 VirtualBox 尝试不同的操作系统时&#xff0c;访问虚拟机中的 USB 驱动器来传输数据非常有用。 安装Guest Additions 自行百度安装Guest Additions的方法&#xff0c;最终的效果如下&#xff1a; 将用户添加到 vboxusers 组…

4.HTML网页开发的工具

4. 网页开发的工具 4.1 快捷键 4.1.1 快速复制一行 快捷键&#xff1a;shiftalt下箭头&#xff08;上箭头&#xff09; 或者ctrlc 然后 ctrlv 4.1.2 选定多个相同的单词 快捷键&#xff1a; ctrld 4.1.3 添加多个光标 快捷键&#xff1a;ctrlalt上箭头&#xff08;下箭头&…

企业云盘与个人云盘:区别与特点一览

企业云盘是企业在寻找文件协同工具的过程中绕不开的一个选项。企业为什么需要专门购置企业网盘&#xff0c;个人云盘能否满足企业的文件协作需求呢&#xff1f;企业云盘和个人云盘有什么区别呢&#xff1f; 企业云盘与个人云盘的区别 1、使用对象&#xff1a;顾名思义&#xf…

Java 简单实现一个 UDP 回显服务器

文章目录 UDP 服务端UDP 客户端实现效果UDP 服务端(实现字典功能)总结 UDP 服务端 package network;import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.SocketException;public class UdpEchoServer {private Da…

CMOS介绍

1 二极管 2 CMOS 2.1 栅极、源极、漏极 2.2 内部结构 2.2 导电原理 - 原理&#xff1a;1.通过门级和衬底加一个垂直电场Ev&#xff0c;从而在两口井之间形成反形层2.如果加的电场足够强&#xff0c;反形层就可以把source&#xff08;源极&#xff09;和drain&#xff08;漏极…

【项目实践01】【请求的路由转发】

文章目录 前言项目背景实现方案具体实现功能演示 思路延伸1. spring cloud gateway2. 研究路由原理2.1 寻找合适的 Handler2.2 执行 Handler2.3 处理调用结果 参考内容 前言 本系列用来记录一些在实际项目中的小东西&#xff0c;并记录在过程中想到一些小东西&#xff0c;因为…

第十六届山东省职业院校技能大赛高职组“软件测试”赛项规程

第十六届山东省职业院校技能大赛 高职组“软件测试”赛项规程 一、赛项名称 赛项名称&#xff1a;软件测试 赛项组别&#xff1a;高职组 赛项专业大类&#xff1a;电子与信息大类 二、竞赛目的 软件是新一代信息技术的灵魂&#xff0c;是数字经济发展的基础&#xff0c;是…

MySQL join原理及优化

MySQL的JOIN原理是基于索引和算法的。在执行JOIN查询时&#xff0c;MySQL会根据连接字段上的索引来查找匹配的记录。 这种算法在链接查询的时候&#xff0c;驱动表会根据关联字段的索引进行查找&#xff0c;当在索引上找到了符合的值&#xff0c;再回表进行查询&#xff0c;也就…

SharePoint 页面中插入自定义代码

我们都知道 SharePoint 是对页面进行编辑的。 对于一些有编程基础的人来说&#xff0c;可能需要对页面中插入代码&#xff0c;这样才能更好的对页面进行配置。 但是在新版本的 SharePoint modern 页面来说&#xff0c;虽然我们可以插入 Embed 组件。 但是 Embed 组件中是不允…

(一)正点原子I.MX6ULL kernel6.1移植准备

一、概述 学完了正点原子的I.MX6ULL移植&#xff0c;正点原子的教程是基于Ubuntu18&#xff0c;使用的是4.1.15的内核&#xff0c;很多年前的了。NXP官方也发布了新的6.1的内核&#xff0c;以及2022.04的uboot。 本文分享一下基于Ubuntu22.04&#xff08;6.2.0-36-generic&…

【C++】C++入门详解 II【深入浅出 C++入门 这一篇文章就够了】

C入门 七、引用&#xff08;一&#xff09;引用 概念&#xff08;1&#xff09;引用 概念&#xff08;2&#xff09;引用 使用★☆&#xff08;3&#xff09;引用 特性&#xff08;4&#xff09;常引用 &#xff08;二&#xff09;引用的 实际应用 及 其意义☆&#xff08;1&am…

【Nginx】nginx | 微信小程序验证域名配置

【Nginx】nginx | 微信小程序验证域名配置 一、说明二、域名管理 一、说明 小程序需要添加头条的功能&#xff0c;内容涉及到富文本内容显示图片资源存储在minio中&#xff0c;域名访问。微信小程序需要验证才能显示。 二、域名管理 服务器是阿里云&#xff0c;用的宝塔管理…

Word 插入的 Visio 图片显示为{EMBED Visio.Drawing.11} 解决方案

World中&#xff0c;如果我们插入了Visio图还用了Endnote&#xff0c; 就可能出现&#xff1a;{EMBED Visio.Drawing.11}问题 解决方案&#xff1a; 1.在相应的文字上右击&#xff0c;在出现的快捷菜单中单击“切换域代码”&#xff0c;一个一个的修复。 2.在菜单工具–>…

探索项目管理软件的多重用途和益处

项目管理软件俨然成了当下项目管理话题中的热门词条&#xff0c;作为一个辅助性管理工具&#xff0c;项目管理软件有什么用&#xff1f;真的值得购入吗&#xff1f; 什么是项目管理软件 顾名思义&#xff0c;项目管理软件就是指在项目管理过程使用的各种软件工具。项目管理软件…

spring-cloud-stream

系列文章目录 第一章 Java线程池技术应用 第二章 CountDownLatch和Semaphone的应用 第三章 Spring Cloud 简介 第四章 Spring Cloud Netflix 之 Eureka 第五章 Spring Cloud Netflix 之 Ribbon 第六章 Spring Cloud 之 OpenFeign 第七章 Spring Cloud 之 GateWay 第八章 Sprin…

【MongoDB】索引 – 文本索引(用权重控制搜索结果)

一、准备工作 这里准备一些数据 db.books.drop();db.books.insert({_id: 1, name: "Java", alias: "java 入门", description: "入门图书" }); db.books.insert({_id: 2, name: "C", alias: "c", description: "C 入…

【算法专题】双指针—三数之和

力扣题目链接&#xff1a;三数之和 一、题目解析 二、算法原理 解法一&#xff1a;排序暴力枚举利用set去重 代码就不写了&#xff0c;你们可以试着写一下 解法二&#xff1a;排序双指针 这题和上一篇文章的两数字和方法类似 排序固定一个数a在这个数的后面区间&#xff0…