Spring Boot集成Druid出現異常報錯的原因及解決

Spring Boot集成Druid異常

在Spring Boot集成Druid項目中,發現錯誤日志中頻繁的出現如下錯誤信息:

discard long time none received connection. , jdbcUrl : jdbc:mysql://******?useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8, version : 1.2.3, lastPacketReceivedIdleMillis : 172675

經過排查發現是Druid版本導致的異常,在1.2.2及以前版本並未出現如此異常。而在其以上版本均存在此問題,下面就來分析一下異常原因及解決方案。

異常分析

首先上面的異常並不影響程序的正常運行,但作為程序員看到程序中不停的出現異常還是難以忍受的。所以還是要刨根問底的解決一下的。

跟蹤堆棧信息會發現對應的異常是從com.alibaba.druid.pool.DruidAbstractDataSource#testConnectionInternal方法中拋出的,對應的代碼如下:

if (valid && isMySql) { // unexcepted branch
    long lastPacketReceivedTimeMs = MySqlUtils.getLastPacketReceivedTimeMs(conn);
    if (lastPacketReceivedTimeMs > 0) {
        long mysqlIdleMillis = currentTimeMillis - lastPacketReceivedTimeMs;
        if (lastPacketReceivedTimeMs > 0 //
                && mysqlIdleMillis >= timeBetweenEvictionRunsMillis) {
            discardConnection(holder);
            String errorMsg = "discard long time none received connection. "
                    + ", jdbcUrl : " + jdbcUrl
                    + ", jdbcUrl : " + jdbcUrl
                    + ", lastPacketReceivedIdleMillis : " + mysqlIdleMillis;
            LOG.error(errorMsg);
            return false;
        }
    }
}

上述代碼中,MySqlUtils.getLastPacketReceivedTimeMs(conn) 是獲取上一次使用的時間,mysqlIdleMillis 就是計算出來空閑的時間,timeBetweenEvictionRunsMillis 是常量60秒。如果連接空閑瞭60秒以上,那就discardConnection(holder) 丟棄這個舊連接並順帶打印瞭一個日志LOG.warn(errorMsg)。

原理追蹤

在上述代碼中,我們看到進入該業務邏輯是有前提條件的,也就是valid和isMySql變量同時為true。isMySql為true是必須的,我們使用的本身就是Mysql數據庫。那麼是否可以讓valid為false呢?這樣不就不會進入該業務處理瞭嗎?

來看看valid的來源,還是在該方法的上面:

boolean valid = validConnectionChecker.isValidConnection(conn, validationQuery, validationQueryTimeout);

我們找到validConnectionChecker的Mysql實現子類MySqlValidConnectionChecker,該類中對isValidConnection的實現如下:

public boolean isValidConnection(Connection conn, String validateQuery, int validationQueryTimeout) throws Exception {
    if (conn.isClosed()) {
        return false;
    }

    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);
    }

}

我們可以看到上述方法中有三個返回的地方:第一個連接已關閉;第二個使用ping的形式進行檢查;第三,使用select 1的方式進行檢查。而使用ping的形式檢查時,無論是否拋異常都會返回true。這裡我們禁用該模式即可。

進入ping的業務邏輯主要靠變量usePingMethod來判斷,追蹤代碼會發現在這裡進行的設置:

public void configFromProperties(Properties properties) {
    String property = properties.getProperty("druid.mysql.usePingMethod");
    if ("true".equals(property)) {
        setUsePingMethod(true);
    } else if ("false".equals(property)) {
        setUsePingMethod(false);
    }
}

那麼,也就是說,當我們把系統屬性druid.mysql.usePingMethod設置為false即可禁用該功能。

禁用Ping Method

找到瞭問題的根源,那麼剩下的就是如何禁用瞭,通常有三種形式。

第一,在啟動程序時在運行參數中增加:-Ddruid.mysql.usePingMethod=false。

第二,在Spring Boot項目中,可在啟動類中添加如下靜態代碼快:

static {
    System.setProperty("druid.mysql.usePingMethod","false");
}

第三,類文件配置。在項目的DruidConfig類中新增加:

/*
* 解決druid 日志報錯:discard long time none received connection:xxx
* */
@PostConstruct
public void setProperties(){
    System.setProperty("druid.mysql.usePingMethod","false");
}

至此,已可以成功關閉該功能,異常信息再也不會出現瞭。

為什麼要清空空閑60秒以上的連接

猜測,阿裡給數據庫設置的數據庫空閑等待時間是60秒,mysql數據庫到瞭空閑等待時間將關閉空閑的連接,以提升數據庫服務器的處理能力。

MySQL的默認空閑等待時間是8小時,就是「wait_timeout」的配置值。如果數據庫主動關閉瞭空閑的連接,而連接池並不知道,還在使用這個連接,就會產生異常。

以上就是Spring Boot集成Druid出現異常報錯的原因及解決的詳細內容,更多關於Spring Boot集成Druid出現異常的資料請關註WalkonNet其它相關文章!

推薦閱讀: