搞懂TomcatJdbc之连接校验

前言

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

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

TomcatJdbc往期文章:

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

正文

一. 连接校验源码解析

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

public boolean validate(int validateAction) {
    return validate(validateAction, null);
}

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

// 这里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连接校验流程图.jpg

二. 连接校验的场景

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

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

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

1. 获取连接校验

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

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,简化版源码如下所示。

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() 方法中会调用到连接校验的逻辑,如下所示。

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队列中的连接进行校验。定时任务的执行逻辑如下所示。

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,简化版源码如下所示。

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,校验不通过时,底层物理连接会被释放,校验通过时,相当于对连接做了一次保活操作。

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


链接:https://juejin.cn/post/7309687967064211508
 

  • 14
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值