搞懂TomcatJdbc之连接获取

前言

当我们使用类似于MyBatisORM 框架来执行一条SQL 时,其中一步就是会从数据库连接池里获取一个连接,了解从数据库连接池获取连接的过程,对于排查SQL耗时问题具有很大的帮助。

本文将针对TomcatJdbc 数据库连接池获取连接的源码进行学习,Tomcat 版本为9.0.82

TomcatJdbc往期文章:

接口访问超时引发的思考
搞懂TomcatJdbc之连接池初始化

正文

当初始化数据库连接池完毕后,会得到一个ConnectionPool ,后续获取连接均从得到的ConnectionPool 中获取,对应方法为ConnectionPool#getConnection,如下所示。

java 复制代码
public Connection getConnection() throws SQLException {
    // 先从连接池获取数据库连接
    PooledConnection con = borrowConnection(-1,null,null);
    // 为获取出来的连接创建代理对象
    return setupConnection(con);
}

继续跟进ConnectionPool#borrowConnection方法,如下所示。

java 复制代码
private PooledConnection borrowConnection(int wait, String username, String password) throws SQLException {

    if (isClosed()) {
        throw new SQLException("Connection pool closed.");
    }

    long now = System.currentTimeMillis();
    // 从空闲队列获取一个连接
    // 若没有空闲连接则得到null
    PooledConnection con = idle.poll();

    while (true) {
        if (con!=null) {
            // 这里主要是校验一下刚刚获取到的空闲连接
            // 如果校验不通过则会重新连接一次数据库
            PooledConnection result = borrowConnection(now, con, username, password);
            // 借出的连接数加1
            borrowedCount.incrementAndGet();
            if (result!=null) {
                return result;
            }
        }

        // 执行到这里说明需要创建连接
        // size表示当前连接池的总连接数
        // 如果size小于配置的最大连接数则创建一个连接
        if (size.get() < getPoolProperties().getMaxActive()) {
            if (size.addAndGet(1) > getPoolProperties().getMaxActive()) {
                size.decrementAndGet();
            } else {
                // 创建一个连接并校验
                // 校验只有在testOnConnect配置为true时才执行
                return createConnection(now, con, username, password);
            }
        }

        // 执行到这里说明当前无空闲连接可用且无法再继续创建连接
        // 那么后续就进入等待获取连接的状态
        long maxWait = wait;
        // wait配置为-1表示等待时间使用连接池的默认值
        if (wait==-1) {
            // 默认是等待30秒
            maxWait = (getPoolProperties().getMaxWait()<=0)?Long.MAX_VALUE:getPoolProperties().getMaxWait();
        }
        // 计算还需要等待的时间
        long timetowait = Math.max(0, maxWait - (System.currentTimeMillis() - now));
        // 当前正在等待获取连接的线程计数加1
        waitcount.incrementAndGet();
        try {
            // 从空闲队列中超时等待获取连接
            con = idle.poll(timetowait, TimeUnit.MILLISECONDS);
        } catch (InterruptedException ex) {
            if (getPoolProperties().getPropagateInterruptState()) {
                Thread.currentThread().interrupt();
            }
            SQLException sx = new SQLException("Pool wait interrupted.");
            sx.initCause(ex);
            throw sx;
        } finally {
            // 获取到连接或者等待超时都需要将wait减1
            // 表示结束等待
            waitcount.decrementAndGet();
        }
        if (maxWait==0 && con == null) {
            if (jmxPool!=null) {
                jmxPool.notify(org.apache.tomcat.jdbc.pool.jmx.ConnectionPool.POOL_EMPTY, "Pool empty - no wait.");
            }
            // 不等待并且没获取到连接
            // 抛出异常并说明情况
            throw new PoolExhaustedException("[" + Thread.currentThread().getName()+"] " +
                    "NoWait: Pool empty. Unable to fetch a connection, none available["+busy.size()+" in use].");
        }
        if (con == null) {
            if ((System.currentTimeMillis() - now) >= maxWait) {
                if (jmxPool!=null) {
                    jmxPool.notify(org.apache.tomcat.jdbc.pool.jmx.ConnectionPool.POOL_EMPTY, "Pool empty - timeout.");
                }
                // 等到达到了超时时间也没有获取到连接
                // 则抛出异常
                throw new PoolExhaustedException("[" + Thread.currentThread().getName()+"] " +
                    "Timeout: Pool empty. Unable to fetch a connection in " + (maxWait / 1000) +
                    " seconds, none available[size:"+size.get() +"; busy:"+busy.size()+"; idle:"+idle.size()+"; lastwait:"+timetowait+"].");
            } else {
                continue;
            }
        }
    }
}

获取连接的流程是很清晰的,可以归纳如下。

  1. 首先从空闲连接队列idle 中获取一个连接。这里是通过poll() 方法获取,所以得到的连接可能为null
  2. 校验获取到的连接。如果要触发校验连接的动作,需要将testOnBorrow 配置为true,如果校验成功则返回连接,如果校验失败,则会基于当前连接重连一次数据库;
  3. 前面如果都没有获取到连接则尝试新建一个连接。新建连接有一个前提,就是当前连接池的总连接数要小于允许的最大连接数;
  4. 如果无法新建连接则进入超时等待获取连接状态。如果从空闲连接队列idle 中无法获取到一个有效连接且也不允许继续创建连接,则需要在指定时间范围内从idle中等待获取连接。

总结

TomcatJdbc获取连接的整个流程图示意如下。

相关推荐
程序员海军1 分钟前
深度测评:在微信里直接操控 OpenClaw
人工智能·后端
野犬寒鸦9 分钟前
面试常问:HTTP 1.0 VS HTTP 2.0 VS HTTP 3.0 的核心区别及底层实现逻辑
服务器·开发语言·网络·后端·面试
小杨的博客12 分钟前
Java + Selenium实现浏览器打印功能
java·selenium
砍材农夫13 分钟前
多层缓存设计
后端
wefly201714 分钟前
M3U8 播放调试天花板!m3u8live.cn纯网页无广告,音视频开发效率直接拉满
java·前端·javascript·python·音视频
来了老板14 分钟前
超越日志与权限:深度解析Python装饰器原理与高阶实战场景
后端
兆子龙17 分钟前
antd 组件也做了同款效果!深入源码看设计模式在前端组件库的应用
java·前端·架构
祁梦17 分钟前
Redis从入门到入土 --- 黑马点评判断秒杀资格
java·后端
兆子龙18 分钟前
lodash 到 lodash-es 多的不仅仅是后缀!深入源码看 ES Module 带来的性能与体积优化
java·前端·架构
lisus200720 分钟前
GO并发统计文件大小
开发语言·后端·golang