搞懂TomcatJdbc之连接校验

前言

TomcatJdbc数据库连接池中的连接,会在被使用的各个阶段进行校验,以确保连接是一个有效可用的连接。

本文将结合TomcatJdbc 连接校验相关配置和源码,对连接校验的原理进行学习,Tomcat 版本为9.0.82

TomcatJdbc往期文章:

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

正文

一. 连接校验源码解析

TomcatJdbc 数据库连接池中的连接,类型为PooledConnection ,其持有一个真实的数据库物理连接java.sql.ConnectionPooledConnection 提供了一组公共方法名为validate() ,用于对当前PooledConnection 进行校验,而validate() 方法也正是TomcatJdbc 数据库连接池中的连接的校验的统一入口 ,即无论哪种情况下的校验,均会调用到PooledConnection#validate 方法。下面看一下PooledConnection#validate方法的实现,如下所示。

java 复制代码
public boolean validate(int validateAction) {
    return validate(validateAction, null);
}

继续跟进重载的validate() 方法,如下所示。

java 复制代码
// 这里validateAction可以为如下值:
// VALIDATE_BORROW(1),表示获取连接校验
// VALIDATE_RETURN(2),表示归还连接校验
// VALIDATE_IDLE(3),表示空闲连接校验
// VALIDATE_INIT(4),表示初始化连接校验
public boolean validate(int validateAction, String sql) {
    // 被丢弃的连接直接校验失败
    if (this.isDiscarded()) {
        return false;
    }

    // 判断当前的校验类型是否有必要进行
    // 没必要进行的校验则直接校验通过
    if (!doValidate(validateAction)) {
        return true;
    }

    // 在配置了校验间隔以及校验类型不是初始化连接校验的情况下
    // 需要判断当前距离连接上一次校验的时间是否小于校验间隔
    // 若小于则直接校验通过
    long now = System.currentTimeMillis();
    if (validateAction!=VALIDATE_INIT &&
        poolProperties.getValidationInterval() > 0 &&
        (now - this.lastValidated) <
        poolProperties.getValidationInterval()) {
        return true;
    }

    // 若配置了Validator则使用Validator来校验连接
    if (poolProperties.getValidator() != null) {
        if (poolProperties.getValidator().validate(connection, validateAction)) {
            this.lastValidated = now;
            return true;
        } else {
            if (getPoolProperties().getLogValidationErrors()) {
                log.error("Custom validation through "+poolProperties.getValidator()+" failed.");
            }
            return false;
        }
    }

    // 通常不会配置Validator
    // 所以通过执行query语句进行连接校验
    String query = sql;

    // 如果是初始化连接校验且配置了InitSQL
    // 则query语句使用配置的InitSQL
    if (validateAction == VALIDATE_INIT && poolProperties.getInitSQL() != null) {
        query = poolProperties.getInitSQL();
    }

    if (query == null) {
        // 否则使用配置的校验连接的SQL
        query = poolProperties.getValidationQuery();
    }

    ......

    boolean transactionCommitted = false;
    Statement stmt = null;
    try {
        // 通过物理连接创建出Statement
        stmt = connection.createStatement();

        // 获取配置的校验连接超时时间
        // 默认的校验连接超时时间为-1
        int validationQueryTimeout = poolProperties.getValidationQueryTimeout();
        if (validationQueryTimeout > 0) {
            stmt.setQueryTimeout(validationQueryTimeout);
        }

        // 执行校验SQL
        stmt.execute(query);
        stmt.close();
        this.lastValidated = now;
        transactionCommitted = silentlyCommitTransactionIfNeeded();
        return true;
    } catch (Exception ex) {
        
        ......

    } finally {
        if (!transactionCommitted) {
            silentlyRollbackTransactionIfNeeded();
        }
    }
    return false;
}

上述校验流程总结如下。

  1. 先判断被校验连接是否已经被丢失。通过判断连接的discarded 字段是否为true,来判断连接是否已经被丢弃,被丢弃的连接直接校验失败;

  2. 然后判断当前校验类型是否需要进行。validate() 方法作为如下四种校验的统一校验入口,需要根据validate() 方法的入参validateAction以及配置来判断当前校验是否需要进行:

    一. VALIDATE_BORROW (1)。表示获取连接校验,validateAction 等于VALIDATE_BORROW ,且将testOnBorrow 配置为true 时校验可以进行;

    二. VALIDATE_RETURN (2)。表示归还连接校验,validateAction 等于VALIDATE_RETURN ,且将testOnReturn 配置为true 时校验可以进行;

    三. VALIDATE_IDLE (3)。表示空闲连接校验,validateAction 等于VALIDATE_IDLE ,且将testWhileIdle 配置为true 时校验可以进行;

    四. VALIDATE_INIT (4)。表示初始化连接校验,validateAction 等于VALIDATE_INIT ,且将testOnConnect 配置为true时校验可以进行。

    校验被判断不需要进行时,直接返回true,表示校验通过;

  3. 然后判断连接是否满足校验间隔。在配置了校验间隔validationInterval 以及校验类型不是初始化连接校验VALIDATE_INIT的情况下,需要判断当前距离连接上一次校验的时间是否小于校验间隔,若小于则表示校验过于频繁,此时直接校验通过,若大于等于则允许校验;

  4. 然后确定校验连接的方式。如果配置了Validator ,则使用Validator 来校验连接,但通常不会配置Validator ,此时通过执行query 语句来校验连接。query 语句在初始化连接校验场景下且配置了InitSQL 时取值为InitSQL ,否则query 就取配置的validationQuery ,如果没有配置validationQuery ,此时会使用Springboot 提供的DatabaseDriver 里面预置的校验语句:/ * ping */ SELECT 1

  5. 最后使用连接来执行query。如果执行成功,则校验成功,否则校验失败。

流程图如下所示。

二. 连接校验的场景

在上一节中已经知道,在TomcatJdbc中有如下四种情况会触发连接校验。

  1. 获取连接时且将testOnBorrow 配置为true
  2. 归还连接时且将testOnReturn 配置为true
  3. 连接空闲时且将testWhileIdle 配置为true
  4. 创建连接时且将testOnConnect 配置为true

那么本节将说明一下如上的几种连接的校验场景。

1. 获取连接校验

当配置testOnBorrowtrue时,每次从连接池获取连接都会校验一下连接,简化版源码如下所示。

java 复制代码
protected PooledConnection borrowConnection(long now, PooledConnection con, String username, String password) throws SQLException {
    boolean setToNull = false;
    try {
        
        ......

        if (!forceReconnect) {
            // 在这里校验连接
            if ((!con.isDiscarded()) && con.validate(PooledConnection.VALIDATE_BORROW)) {
                
                ......

                return con;
            }
        }
        
        // 执行到这里说明连接检验失败
        try {
            // 重新连接一次数据库得到物理连接
            con.reconnect();
            reconnectedCount.incrementAndGet();
            int validationMode = isInitNewConnections() ?
                    PooledConnection.VALIDATE_INIT:
                    PooledConnection.VALIDATE_BORROW;
            if (con.validate(validationMode)) {
                
                ......

                return con;
            } else {
                
                ......

            }
        } catch (Exception x) {

            ......

        }
    } finally {
        
        ......

    }
}

通过上面源码还能知道,当连接校验不通过时,会重新去连接一次数据库,然后当前连接持有的物理连接就替换为重新建立的连接。

2. 归还连接校验

当配置testOnReturntrue 时,将连接归还到连接池会校验一下连接。连接的归还,对应方法为ConnectionPool#returnConnection,简化版源码如下所示。

java 复制代码
protected void returnConnection(PooledConnection con) {
    
    ......

    if (con != null) {
        try {
            
            ......

            // 将连接从busy队列移除
            if (busy.remove(con)) {
                // 在shouldClose()会进行归还连接校验
                if (!shouldClose(con,PooledConnection.VALIDATE_RETURN) && reconnectIfExpired(con)) {
                    
                    ......

                } else {
                    // 归还连接校验不通过时需要释放底层物理连接
                    release(con);
                }
            } else {
                
                ......

                release(con);
            }
        } finally {
            con.unlock();
        }
    }
}

在将连接归还到数据库连接池时,会调用到shouldClose() 方法来判断连接底层的物理连接是否需要关闭,在shouldClose() 方法中会调用到连接校验的逻辑,如下所示。

java 复制代码
protected boolean shouldClose(PooledConnection con, int action) {
    if (con.getConnectionVersion() < getPoolVersion()) {
        return true;
    }
    if (con.isDiscarded()) {
        return true;
    }
    if (isClosed()) {
        return true;
    }
    // 在这里进行归还连接校验
    if (!con.validate(action)) {
        return true;
    }
    if (!terminateTransaction(con)) {
        return true;
    }
    return false;
}

如果归还连接校验失败,则shouldClose() 方法会返回false ,从而会调用到ConnectionPool#release方法来释放底层物理连接。

3. 空闲连接校验

当配置testWhileIdletrue 时,TomcatJdbc 会在一个定时任务中对idle队列中的连接进行校验。定时任务的执行逻辑如下所示。

java 复制代码
public void run() {
    ConnectionPool pool = this.pool.get();
    if (pool == null) {
        stopRunning();
    } else if (!pool.isClosed()) {
        try {
            if (pool.getPoolProperties().isRemoveAbandoned()
                    || pool.getPoolProperties().getSuspectTimeout() > 0) {
                pool.checkAbandoned();
            }
            if (pool.getPoolProperties().getMinIdle() < pool.idle
                    .size()) {
                pool.checkIdle();
            }
            if (pool.getPoolProperties().isTestWhileIdle()) {
                // 在这里校验链接
                pool.testAllIdle(false);
            } else if (pool.getPoolProperties().getMaxAge() > 0) {
                pool.testAllIdle(true);
            }
        } catch (Exception x) {
            log.error("", x);
        }
    }
}

上述的连接校验,如果校验不通过,则连接对应的底层物理连接会被释放,如果校验通过,就相当于对连接做了一次保活操作。

4. 创建连接校验

当配置testOnConnecttrue 时,每次创建新连接时,都会进入到连接校验逻辑,对应方法为ConnectionPool#createConnection,简化版源码如下所示。

java 复制代码
protected PooledConnection createConnection(long now, PooledConnection notUsed, String username, String password) throws SQLException {
    // 创建一个空连接
    // 即底层物理连接还没建立
    PooledConnection con = create(false);
    
    ......

    boolean error = false;
    try {
        con.lock();
        // 实际建立底层物理连接
        con.connect();
        // 在这里执行创建连接校验
        if (con.validate(PooledConnection.VALIDATE_INIT)) {
            
            ......

            return con;
        } else {
            throw new SQLException("Validation Query Failed, enable logValidationErrors for more details.");
        }
    } catch (Exception e) {
        
        ......

    } finally {
        
        ......

    }
}

如果校验通过,就返回这个新建立的连接,如果校验失败,则抛出异常告诉外层本次连接创建失败。

总结

TomcatJdbc 有着较为完善且灵活的连接校验机制,在获取连接归还连接创建连接连接空闲时都提供了配置来决定是否进行校验。

对于获取连接校验,开启条件是配置testOnBorrowtrue,校验不通过时,会重连一次数据库得到底层物理连接。

对于归还连接校验,开启条件是配置testOnReturntrue,校验不通过时,会释放底层物理连接。

对于创建连接校验,开启条件是配置testOnConnecttrue,校验不通过时,本次连接创建会失败。

对于空闲连接校验,开启条件是配置testWhileIdletrue,校验不通过时,底层物理连接会被释放,校验通过时,相当于对连接做了一次保活操作。

获取连接校验,归还连接校验以及创建连接校验,均是在业务线程中,所以通常推荐只开启空闲连接校验,让后台线程完成连接校验和保活的工作即可。

相关推荐
程序员南飞34 分钟前
ps aux | grep smart_webrtc这条指令代表什么意思
java·linux·ubuntu·webrtc
弥琉撒到我37 分钟前
微服务swagger解析部署使用全流程
java·微服务·架构·swagger
一颗花生米。1 小时前
深入理解JavaScript 的原型继承
java·开发语言·javascript·原型模式
问道飞鱼1 小时前
Java基础-单例模式的实现
java·开发语言·单例模式
ok!ko5 小时前
设计模式之原型模式(通俗易懂--代码辅助理解【Java版】)
java·设计模式·原型模式
2401_857622666 小时前
SpringBoot框架下校园资料库的构建与优化
spring boot·后端·php
2402_857589366 小时前
“衣依”服装销售平台:Spring Boot框架的设计与实现
java·spring boot·后端
吾爱星辰6 小时前
Kotlin 处理字符串和正则表达式(二十一)
java·开发语言·jvm·正则表达式·kotlin
哎呦没7 小时前
大学生就业招聘:Spring Boot系统的架构分析
java·spring boot·后端
_.Switch7 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j