手撕数据库连接池

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操作。

 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【探索】HelloGitHub上面推荐几个程序我的真实体验与收藏

第一个项目——体验成为操作系统 youre-the-os&#xff1a;模拟计算机操作系统的游戏。这是一个 Python 写的 Web 游戏&#xff0c;在游戏中玩家扮演的是一台计算机的操作系统。玩家必须通过管理进程、内存和 I/O 事件&#xff0c;不让进程闲置太久&#xff0c;因为进程等待太…

Linux系统服务器日常运维常用系统监控指令

常用系统监控指令 1. top命令1.1 命令语法1.2 命令选项1.3 命令详解 2. free命令2.1 命令语法2.2 命令选项2.3 命令详解 3. df命令3.1 命令语法3.2 命令选项3.3 命令详解 4. ps命令4.1 命令语法4.2 命令选项4.3 命令详解 5. crontab命令6. 查看端口命令6.1 netstat命令6.2 lsof…

RFID智慧物流设计解决方案

物流行业需求 物流是将物质资料从供应者运送到需求者的物理运动过程&#xff0c;涉及运输、保管、包装、装卸、流通加工、配送以及信息等多个基本活动的统一整合&#xff0c;在经济全球化和电子商务的推动下&#xff0c;快递物流和医药物流成为现代物流的两大重要产业。随着智…

报错信息Update your application‘s configuration

在使用Maven项目时&#xff0c;有一个报错信息是&#xff1a;Update your applications configuration 这类问题&#xff0c;就是我们的application.yml文件 或者 application.properties文件 内容哪里写错了 我的问题是格式对齐方式出错&#xff0c;如下&#xff1a; 修改过后…

Javascript知识点详解:对象的继承、原型对象、原型链

目录 对象的继承 原型对象概述 构造函数的缺点 prototype 属性的作用 原型链 constructor 属性 instanceof 运算符 构造函数的继承 多重继承 对象的继承 面向对象编程很重要的一个方面&#xff0c;就是对象的继承。A 对象通过继承 B 对象&#xff0c;就能直接拥有 B …

PHP的curl会话

介绍: Curl&#xff08;Client for URLs&#xff09;在PHP中是一个强大而灵活的工具&#xff0c;用于进行各种网络请求。PHP中的Curl库允许开发者通过代码模拟HTTP请求、与API交互、进行数据传输等。在这里&#xff0c;我们将详细解析PHP中Curl会话的各个方面&#xff0c;涵盖…

Ubuntu18.04 安装docker教程

Ubuntu18.04 安装docker教程 1、前言 Docker Engine-Community 支持以下的 Ubuntu 版本&#xff1a; Xenial 16.04 (LTS)Bionic 18.04 (LTS)Cosmic 18.10Disco 19.04 Docker Engine-Community 支持以下CPU架构&#xff1a; x86_64&#xff08;或 amd64&#xff09;armhfarm…

npm install 报错 chromedriver 安装失败的解决办法

npm install chromedriver --chromedriver_cdnurlhttp://cdn.npm.taobao.org/dist/chromedriver

c语言break和continue语句用法

作用 break语句&#xff1a;可用于循环结构和开关结构(switch)中&#xff0c;在开关语句中的作用是执行完当前case后立即跳出switch结构。在循环语句中的作用是终止当前层的循环。continue语句&#xff1a;作用是跳过循环体中剩余的语句而强行执行下一次循环。 区别 continue…

需求调研,是做好商业智能BI的第一步

商业智能BI&#xff0c;一个高大上的名字&#xff0c;一直被很多人认为是企业信息化中的“面子工程”。美其名曰“可视化大屏”&#xff0c;什么经营驾驶舱&#xff0c;什么管理仪表盘&#xff0c;都是花里胡哨的东西&#xff0c;老板不会看&#xff0c;企业不会用&#xff0c;…

java项目之果蔬经营平台系统(ssm框架)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于ssm的果蔬经营平台系统。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 项目简介&#xff1a; 管理员&#xff1a;首页、个人…

【每日一题】—— C. Anonymous Informant(Codeforces Round 908 (Div. 2))(思维题)

&#x1f30f;博客主页&#xff1a;PH_modest的博客主页 &#x1f6a9;当前专栏&#xff1a;每日一题 &#x1f48c;其他专栏&#xff1a; &#x1f534; 每日反刍 &#x1f7e1; C跬步积累 &#x1f7e2; C语言跬步积累 &#x1f308;座右铭&#xff1a;广积粮&#xff0c;缓称…

视频批量剪辑:AI智剪入门,轻松掌握智能剪辑技巧

在数字媒体时代&#xff0c;视频剪辑已经成为一项必备的技能。无论是为了工作需要&#xff0c;还是为了在社交媒体上分享生活&#xff0c;掌握视频剪辑技巧都能为我们的生活和工作带来很多便利。然而&#xff0c;对于初学者来说&#xff0c;视频剪辑可能是一项艰巨的任务。现在…

2023年11月上旬大模型新动向集锦

2023年11月上旬大模型新动向集锦 2023.11.10版权声明&#xff1a;本文为博主chszs的原创文章&#xff0c;未经博主允许不得转载。 1、GPT-4 Turbo在中文基准评测获八项满分 基于SuperCLUE通用大模型综合性中文测评基准&#xff0c;测评人员对GPT-4 Turbo进行了全方位测评。测…

十进制转换成2进制

十进制转换成2进制 参考链接&#xff1a;https://blog.csdn.net/qq_44755403/article/details/89279970?ops_request_misc%257B%2522request%255Fid%2522%253A%2522169960944816800227457337%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id…

2024 款:最新前端技术趋势

Hello&#xff0c;大家好&#xff0c;我是 Sunday。 上一次的时候聊了 那么些已经落后的前端开发技术 。但是光知道什么技术落后了是不够的&#xff0c;咱们还得知道 前端最新的技术趋势是什么。所以&#xff0c;今天这篇文章&#xff0c;咱们就来聊一聊&#xff0c;2023 最新…

递归和递推

文章目录 数楼梯题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1 提示 [NOIP2002 普及组] 过河卒题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1 提示 [NOIP2003 普及组] 栈题目背景题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1 提示 数楼梯 题目…

MySQL单表过大、主从模式、同步模式优化原理

文章目录 MYSQL单表数据达2000万性能严重下降?前言InnoDB索引数据结构B树 Sharding Sphere分库分表Sharding-JDBCSharding-JDBC的相关概念说明逻辑表广播表绑定表 Sharding-JDBC中的分片策略自动分片算法取模分片算法哈希取模分片算法分片容量范围标准分片算法行表达式分片算法…

i5、i9被取消,intel第一代酷睿Ultra CPU规格出炉

早在今年 6 月&#xff0c;Intel 就公布了即将带来全新一代酷睿 Ultra CPU。 纵观 Intel CPU 历史上的数次改名&#xff0c;几乎每次都代表了产品大变革&#xff0c;性能也是跟着跨越性地水涨船高。 而如今再次抛弃沿用长达十多年的酷睿 i 系改名为酷睿 Ultra&#xff0c;似乎…

kgm格式怎么转换为mp3?这样操作真的很简单!

kgm格式是一种酷狗音乐的音频格式&#xff0c;是酷狗为了保护音乐版权而专门创建的一种加密格式。这种格式只能在酷狗音乐播放器上面播放&#xff0c;那么如何把他转换成兼容性更高的MP3音频格式呢&#xff1f;下面介绍了三种常用的方法。 方法一&#xff1a;野葱视频转换器 1…