Druid-连接检查机制

1,140 阅读6分钟

1.Druid提供的校验参数

Druid对于连接校验提供了六个参数,其中testOnBorrowtestOnReturntestWhileIdle是Druid所提供的连接校验时点,其余三个参数为校验的相关配置。

配置缺省值说明
validationQuery用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
validationQueryTimeout单位:秒,检测连接是否有效的超时时间。底层调用jdbc Statement对象的void setQueryTimeout(int seconds)方法
testOnBorrowfalse申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
testOnReturnfalse归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
testWhileIdletrue建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
timeBetweenEvictionRunsMillis1分钟有两个含义:
(1) Destroy线程会检测连接的间隔时间,如果连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接。
(2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明

2. Druid是如何进行连接校验的?

Druid依靠testConnectionInternal方法进行连接校验,如果当前连接池存在validConnectionChecker,则会利用validConnectionChecker进行校验,否则将利用一些内部状态以及执行校验SQL的方式进行探活,主要的校验方式都是以执行校验SQL进行校验,而MySQL可以使用PingMethod校验连接的有效性。

/**
 * 校验连接是否有效
 * @param holder
 * @param conn
 * @return
 */
protected boolean testConnectionInternal(DruidConnectionHolder holder, Connection conn) {
    //此部分似乎是为了做监控用的
    String sqlFile = JdbcSqlStat.getContextSqlFile();
    String sqlName = JdbcSqlStat.getContextSqlName();

    if (sqlFile != null) {
        JdbcSqlStat.setContextSqlFile(null);
    }
    if (sqlName != null) {
        JdbcSqlStat.setContextSqlName(null);
    }
    try {
        //判断当前连接池所持有的检查器是否为空
        if (validConnectionChecker != null) {
            //调用检查器的教研方法进行教研
            boolean valid = validConnectionChecker.isValidConnection(conn, validationQuery, validationQueryTimeout);
            long currentTimeMillis = System.currentTimeMillis();
            //设置相关的监控参数
            if (holder != null) {
                holder.lastValidTimeMillis = currentTimeMillis;
                holder.lastExecTimeMillis = currentTimeMillis;
            }
            //如果校验通过并且是MySQL的情况下
            if (valid && isMySql) { // unexcepted branch
                //获得MySQL服务器中这个会话最后一次执行语句时间
                long lastPacketReceivedTimeMs = MySqlUtils.getLastPacketReceivedTimeMs(conn);
                //如果这个值>0,即工具类返回正常结果的情况下
                if (lastPacketReceivedTimeMs > 0) {
                    //获得MySQL的空闲时间
                    long mysqlIdleMillis = currentTimeMillis - lastPacketReceivedTimeMs;
                    //如果服务端空闲时间>timeBetweenEvictionRunsMillis,则丢弃该连接,此时校验失败
                    if (lastPacketReceivedTimeMs > 0 //
                            && mysqlIdleMillis >= timeBetweenEvictionRunsMillis) {
                        discardConnection(holder);
                        String errorMsg = "discard long time none received connection. "
                                + ", jdbcUrl : " + jdbcUrl
                                + ", version : " + VERSION.getVersionNumber()
                                + ", lastPacketReceivedIdleMillis : " + mysqlIdleMillis;
                        LOG.warn(errorMsg);
                        return false;
                    }
                }
            }
            //如果校验成功,并且连接池中其他连接发生过异常
            if (valid && onFatalError) {
                //加锁并且重置状态
                lock.lock();
                try {
                    if (onFatalError) {
                        onFatalError = false;
                    }
                } finally {
                    lock.unlock();
                }
            }
            //返回校验结果
            return valid;
        }

        //判断当前连接是否被关闭(DruidPooledConnection内部状态),如果关闭则连接无效
        if (conn.isClosed()) {
            return false;
        }
        //如果当前没有配置校验语句,则认为该连接有效
        if (null == validationQuery) {
            return true;
        }

        Statement stmt = null;
        ResultSet rset = null;
        try {
            //创建语句对象
            stmt = conn.createStatement();
            //设置该语句超时响应时间,如果没有配置则不进行操作
            if (getValidationQueryTimeout() > 0) {
                stmt.setQueryTimeout(validationQueryTimeout);
            }
            //执行校验语句
            rset = stmt.executeQuery(validationQuery);
            //如果没有返回结果,则校验失败,连接失效
            if (!rset.next()) {
                return false;
            }
        } finally {
            //关闭校验时所创建的语句和返回对象
            JdbcUtils.close(rset);
            JdbcUtils.close(stmt);
        }
        //判断致命异常状态并重置
        if (onFatalError) {
            lock.lock();
            try {
                if (onFatalError) {
                    onFatalError = false;
                }
            } finally {
                lock.unlock();
            }
        }
        //如果上方分支都不存在问题,则该链接有效
        return true;
    } catch (Throwable ex) {
        // skip
        return false;
    } finally {
        if (sqlFile != null) {
            JdbcSqlStat.setContextSqlFile(sqlFile);
        }
        if (sqlName != null) {
            JdbcSqlStat.setContextSqlName(sqlName);
        }
    }
}

2.1 validConnectionChecker

validConnectionChecker是Druid针对不同数据库所提供不同的检查器,是ValidConnectionChecker接口的实现类,默认提供MySqlORACLESQL_SERVERPOSTGRESQL以及OCEANBASE的检查器,这些检查器中提供了默认的validationQuery,用于在没有配置validationQuery的时候进行处理。

这些检查器将在连接池初始化的时候被自动识别加载。

public void init() throws SQLException {
		...省略部分代码...
        initExceptionSorter();
    	//调用方法初始化连接检查器
        initValidConnectionChecker();
        validationQueryCheck();
		...省略部分代码...
}

Druid通过驱动类名进行判断,自动识别对应的检查器。

private void initValidConnectionChecker() {
    //如果已经设置过检查器,则不继续执行
    if (this.validConnectionChecker != null) {
        return;
    }
    //获得当前驱动的类名
    String realDriverClassName = driver.getClass().getName();
    //对不同类型数据库进行判断并设置对应的检查器
    if (JdbcUtils.isMySqlDriver(realDriverClassName)) {
        this.validConnectionChecker = new MySqlValidConnectionChecker(usePingMethod);

    } else if (realDriverClassName.equals(JdbcConstants.ORACLE_DRIVER)
               || realDriverClassName.equals(JdbcConstants.ORACLE_DRIVER2)) {
        this.validConnectionChecker = new OracleValidConnectionChecker();

    } else if (realDriverClassName.equals(JdbcConstants.SQL_SERVER_DRIVER)
               || realDriverClassName.equals(JdbcConstants.SQL_SERVER_DRIVER_SQLJDBC4)
               || realDriverClassName.equals(JdbcConstants.SQL_SERVER_DRIVER_JTDS)) {
        this.validConnectionChecker = new MSSQLValidConnectionChecker();

    } else if (realDriverClassName.equals(JdbcConstants.POSTGRESQL_DRIVER)
               || realDriverClassName.equals(JdbcConstants.ENTERPRISEDB_DRIVER)
               || realDriverClassName.equals(JdbcConstants.POLARDB_DRIVER)) {
        this.validConnectionChecker = new PGValidConnectionChecker();
    } else if (realDriverClassName.equals(JdbcConstants.OCEANBASE_DRIVER)
               || (realDriverClassName.equals(JdbcConstants.OCEANBASE_DRIVER2))) {
        DbType dbType = DbType.of(this.dbTypeName);
        this.validConnectionChecker = new OceanBaseValidConnectionChecker(dbType);
    }

}

2.2 独特的MySqlValidConnectionChecker

在MySQL检查器中,存在一个特殊的属性usePingMethod,如果开启了这个属性,MySQL检查器将不会通过执行SQL的方式对连接进行校验,而是会通过MySQL所提供的pingInternal方法发送ping进行校验,该属性通过连接池中的属性进行传递,默认为false,且暂不支持参数配置,需要自行调用set方法进行设置。

public class MySqlValidConnectionChecker extends ValidConnectionCheckerAdapter implements ValidConnectionChecker, Serializable {

    public static final int DEFAULT_VALIDATION_QUERY_TIMEOUT = 1;
    public static final String DEFAULT_VALIDATION_QUERY = "SELECT 1";

    private static final long serialVersionUID = 1L;
    private static final Log  LOG              = LogFactory.getLog(MySqlValidConnectionChecker.class);

    private Class<?> clazz;
    private Method   ping;
    private boolean  usePingMethod = false;

    public MySqlValidConnectionChecker(boolean usePingMethod){
        try {
            //通过反射的方式获得pingInternal的实例
            clazz = Utils.loadClass("com.mysql.jdbc.MySQLConnection");
            if (clazz == null) {
                clazz = Utils.loadClass("com.mysql.cj.jdbc.ConnectionImpl");
            }

            if (clazz != null) {
                ping = clazz.getMethod("pingInternal", boolean.class, int.class);
            }
			//如果没有加载成功的话,则依旧会使用SQL进行校验
            if (ping != null && usePingMethod == true) {
                this.usePingMethod = true;
            }
        } catch (Exception e) {
            LOG.warn("Cannot resolve com.mysql.jdbc.Connection.ping method.  Will use 'SELECT 1' instead.", e);
        }

        configFromProperties(System.getProperties());
    }


    public boolean isValidConnection(Connection conn, String validateQuery, int validationQueryTimeout) throws Exception {
        if (conn.isClosed()) {
            return false;
        }
        //判断是否使用ping进行校验
        if (usePingMethod) {
            if (conn instanceof DruidPooledConnection) {
                conn = ((DruidPooledConnection) conn).getConnection();
            }

            if (conn instanceof ConnectionProxy) {
                conn = ((ConnectionProxy) conn).getRawObject();
            }
            //判断连接对象和连接对象的关系是否可以转化(是一样的类/接口或超类/超接口)
            if (clazz.isAssignableFrom(conn.getClass())) {
                if (validationQueryTimeout <= 0) {
                    validationQueryTimeout = DEFAULT_VALIDATION_QUERY_TIMEOUT;
                }

                try {
                    //执行方法进行校验
                    ping.invoke(conn, true, validationQueryTimeout * 1000);
                } catch (InvocationTargetException e) {
                    Throwable cause = e.getCause();
                    if (cause instanceof SQLException) {
                        throw (SQLException) cause;
                    }
                    throw e;
                }
                return true;
            }
        }

        String query = validateQuery;
        if (validateQuery == null || validateQuery.isEmpty()) {
            query = DEFAULT_VALIDATION_QUERY;
        }

        Statement stmt = null;
        ResultSet rs = null;
        try {
            stmt = conn.createStatement();
            if (validationQueryTimeout > 0) {
                stmt.setQueryTimeout(validationQueryTimeout);
            }
            rs = stmt.executeQuery(query);
            return true;
        } finally {
            JdbcUtils.close(rs);
            JdbcUtils.close(stmt);
        }

    }

}

3. testWhileIdle的校验时点

testOnBorrow、testOnReturn两者分别在获得连接/归还连接时进行校验,无特殊内容。

如果以字面意思来看,将会有一个线程或定时器判断链接是否空闲一段时间(timeBetweenEvictionRunsMillis),实际这个时点却是在获得链接时进行的判断。

由于testOnBorrow和testWhileIdle都是检查连接有效性,testOnBorrow是每次获取连接必定检查,testWhileIdle是超过一定时间才检查,所以当开启testOnBorrow时,testWhileIdle将不会执行。

public DruidPooledConnection getConnectionDirect(long maxWaitMillis) throws SQLException {
    int notFullTimeoutRetryCnt = 0;
    for (;;) {
        // handle notFullTimeoutRetry
        DruidPooledConnection poolableConnection;
        try {
            poolableConnection = getConnectionInternal(maxWaitMillis);
        } catch (GetConnectionTimeoutException ex) {
            if (notFullTimeoutRetryCnt <= this.notFullTimeoutRetryCount && !isFull()) {
                notFullTimeoutRetryCnt++;
                if (LOG.isWarnEnabled()) {
                    LOG.warn("get connection timeout retry : " + notFullTimeoutRetryCnt);
                }
                continue;
            }
            throw ex;
        }
        //如果开启了testOnBorrow,则每次获得连接时都执行一次校验
        if (testOnBorrow) {
            boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);
            if (!validate) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("skip not validate connection.");
                }

                discardConnection(poolableConnection.holder);
                continue;
            }
        } else {
            //判断连接是否被关闭,如果被关闭,则销毁当前连接(此时Holder应该为空)
            if (poolableConnection.conn.isClosed()) {
                discardConnection(poolableConnection.holder); // 传入null,避免重复关闭
                continue;
            }
            //如果开启了testWhileIdle,判断当前连接是空闲了所配置的时间,如果没有则不进行校验
            if (testWhileIdle) {
                //得到当前连接的持有者,便于获得统计信息
                final DruidConnectionHolder holder = poolableConnection.holder;
                long currentTimeMillis             = System.currentTimeMillis();
                //得到当前连接上次的活跃时间、上次的执行时间、上次校验存活的时间(归还连接时)
                long lastActiveTimeMillis          = holder.lastActiveTimeMillis;
                long lastExecTimeMillis            = holder.lastExecTimeMillis;
                long lastKeepTimeMillis            = holder.lastKeepTimeMillis;
                //如果开启了checkExecuteTime参数,并且最后执行时间与最后活跃时间不一致,则认为最后的活跃时间是执行时间
                if (checkExecuteTime
                    && lastExecTimeMillis != lastActiveTimeMillis) {
                    lastActiveTimeMillis = lastExecTimeMillis;
                }
                //最后校验存活时间大于上次活跃时间,则取最后校验存活时间
                if (lastKeepTimeMillis > lastActiveTimeMillis) {
                    lastActiveTimeMillis = lastKeepTimeMillis;
                }
                //空闲时间=当前时间-上次活跃时间
                long idleMillis                    = currentTimeMillis - lastActiveTimeMillis;
                //获得timeBetweenEvictionRunsMillis参数
                long timeBetweenEvictionRunsMillis = this.timeBetweenEvictionRunsMillis;
                //如果没有配置这个参数,取默认值,60000ms,即1分钟
                if (timeBetweenEvictionRunsMillis <= 0) {
                    timeBetweenEvictionRunsMillis = DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS;
                }
                //如果空闲时间超过了timeBetweenEvictionRunsMillis或者空闲时间小于0(此时可能发生时钟回拨等问题?)
                if (idleMillis >= timeBetweenEvictionRunsMillis
                    || idleMillis < 0 // unexcepted branch
                   ) {
                    //校验链接状态
                    boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);
                    if (!validate) {
                        if (LOG.isDebugEnabled()) {
                            LOG.debug("skip not validate connection.");
                        }

                        discardConnection(poolableConnection.holder);
                        continue;
                    }
                }
            }
        }

        if (removeAbandoned) {
            StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
            poolableConnection.connectStackTrace = stackTrace;
            poolableConnection.setConnectedTimeNano();
            poolableConnection.traceEnable = true;

            activeConnectionLock.lock();
            try {
                activeConnections.put(poolableConnection, PRESENT);
            } finally {
                activeConnectionLock.unlock();
            }
        }

        if (!this.defaultAutoCommit) {
            poolableConnection.setAutoCommit(false);
        }

        return poolableConnection;
    }
}