搞懂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获取连接的整个流程图示意如下。

相关推荐
苏三说技术10 分钟前
Claude Code从失控到起飞,只用了这些技巧
后端
长栎1 小时前
写 for 循环写了十年,你却从没用过迭代器模式最狠的那一面
后端
LiaCode1 小时前
Redis 在生产项目的使用
前端·后端
用户559822481221 小时前
Docker Compose Down 导致容器数据误删——ext4 日志恢复全记录
后端
LiaCode1 小时前
一天学完 redis 的爽翻版核心知识总结
前端·后端
大刚测试开发实战1 小时前
如何内网穿透访问本地私有化部署的TestHub
前端·后端·github
xiaodaoluanzha2 小时前
迄今為止,最簡單的編程語言 Nolang
前端·后端
Csvn2 小时前
Docker 容器管理入门 — 从镜像到容器编排
后端
用户762352425912 小时前
ShardingJDBC
后端
行者全栈架构师2 小时前
IDEA 中 Maven 项目的 15 个红色报错快速解决方法
java·后端