Druid連接報錯discard long time none received connection

DruidDataSource#getConnection

   @Override
 public DruidPooledConnection getConnection() throws SQLException {
return getConnection(maxWait);
}

public DruidPooledConnection getConnection(long maxWaitMillis) throws SQLException {
 //初始化 所以init方法是第一次獲取連接的時候才會初始化
init();

if (filters.size() > 0) {
  // 如果有攔截器  攔截器鏈 責(zé)任鏈模式執(zhí)行所有的攔截
    FilterChainImpl filterChain = new FilterChainImpl(this);
    // 攔截器獲取數(shù)據(jù)庫連接代碼詳解 , 應(yīng)該也是執(zhí)行完攔截后獲取連接
    return filterChain.dataSource_connect(this, maxWaitMillis);
} else {
   // 直接獲取連接
    return getConnectionDirect(maxWaitMillis);
}

}

//直接獲取連接

public DruidPooledConnection getConnectionDirect(long maxWaitMillis) throws SQLException {
    //超時重試的次數(shù)
int notFullTimeoutRetryCnt = 0;
for (;;) {
    // handle notFullTimeoutRetry
    DruidPooledConnection poolableConnection;
    try {
    //獲取連接的內(nèi)部方法
        poolableConnection = getConnectionInternal(maxWaitMillis);
    } catch (GetConnectionTimeoutException ex) {
        if (notFullTimeoutRetryCnt <= this.notFullTimeoutRetryCount && !isFull()) {
        //重試次數(shù)超過日志打印
            notFullTimeoutRetryCnt++;
            if (LOG.isWarnEnabled()) {
                LOG.warn("get connection timeout retry : " + notFullTimeoutRetryCnt);
            }
            continue;
        }
        throw ex;
    }

    if (testOnBorrow) {
    // 測試連接
        boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);
        if (!validate) {
                //驗(yàn)證異常
            if (LOG.isDebugEnabled()) {
                LOG.debug("skip not validate connection.");
            }
            //關(guān)閉連接
            discardConnection(poolableConnection.holder);
            continue;
        }
    } else {
        if (poolableConnection.conn.isClosed()) {
            discardConnection(poolableConnection.holder); // 傳入null,避免重復(fù)關(guān)閉
            continue;
        }
                    // todo 不太能理解 既然已經(jīng)在init的時候已經(jīng)啟動了守護(hù)線程createAndStartDestroyThread去進(jìn)行空閑線程和超時線程的定時掃描和回收,為什么在獲取連接的時候還要重新處理一次呢???
        if (testWhileIdle) {
          //空閑檢測有效性
            final DruidConnectionHolder holder = poolableConnection.holder;
            long currentTimeMillis             = System.currentTimeMillis();
            long lastActiveTimeMillis          = holder.lastActiveTimeMillis;
            long lastExecTimeMillis            = holder.lastExecTimeMillis;
            long lastKeepTimeMillis            = holder.lastKeepTimeMillis;
            //更新最近活躍時間
            if (checkExecuteTime
                    && lastExecTimeMillis != lastActiveTimeMillis) {
              
                lastActiveTimeMillis = lastExecTimeMillis;
            }

            if (lastKeepTimeMillis > lastActiveTimeMillis) {
                lastActiveTimeMillis = lastKeepTimeMillis;
            }
                            //存活時間
            long idleMillis                    = currentTimeMillis - lastActiveTimeMillis;

            long timeBetweenEvictionRunsMillis = this.timeBetweenEvictionRunsMillis;

            if (timeBetweenEvictionRunsMillis <= 0) {
                //運(yùn)行時間間隔設(shè)置為默認(rèn)時間
                timeBetweenEvictionRunsMillis = DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS;
            }

            if (idleMillis >= timeBetweenEvictionRunsMillis
                    || idleMillis < 0 // unexcepted branch
                    ) {
              //校驗(yàn)是否有效
                boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);
                if (!validate) {
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("skip not validate connection.");
                    }
                                            //關(guān)閉連接
                    discardConnection(poolableConnection.holder);
                     continue;
                }
            }
        }
        }


    if (removeAbandoned) {
      //當(dāng)前連接
        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;
}

druid中找到相關(guān)代碼,查看報錯相關(guān)的變量的邏輯

   if (validConnectionChecker != null) {
     //校驗(yàn)連接狀態(tài),其中的validationQuery就是我們要執(zhí)行的sql語句,validationQueryTimeOut表示執(zhí)行sql的超時時間,valid表示是否檢驗(yàn)成功或者正常響應(yīng)
            boolean valid = validConnectionChecker.isValidConnection(conn, validationQuery, validationQueryTimeout);
             //當(dāng)前系統(tǒng)時間
            long currentTimeMillis = System.currentTimeMillis();
            if (holder != null) {
                holder.lastValidTimeMillis = currentTimeMillis;
                holder.lastExecTimeMillis = currentTimeMillis;
            }
           //如果連接校驗(yàn)成功,并且是mysql數(shù)據(jù)庫的話
            if (valid && isMySql) { // unexcepted branch
                
             //獲取最近一次查詢數(shù)據(jù)庫接受數(shù)據(jù)包的時間
                long lastPacketReceivedTimeMs = MySqlUtils.getLastPacketReceivedTimeMs(conn);
                if (lastPacketReceivedTimeMs > 0) {
           
                //當(dāng)前時間與最后一次接受數(shù)據(jù)的時間差
                    long mysqlIdleMillis = currentTimeMillis - lastPacketReceivedTimeMs;
                  //如果時間差大于druid的配置timeBetweenEvictionRunsMislis就開始報錯。
                 //timeBetweenEvictionRunsMislis的默認(rèn)時間是60秒
                    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;
                    }
                }
            }

通過上述分析,我們要立馬就知道,我們要想不報錯就需要將timeBetweenEvictionRunsMislis的時間變大。于是我將這個值改大了再試試,結(jié)果還是在報錯。
主要是lastPacketReceivedTimeMs 代表的是最后一次接受數(shù)據(jù)包的時間。顯然這里的時間可以是昨天或者很久以前。那么為了控制讓程序不要執(zhí)行這里的報錯和返回false,只能通過修改這里的valid了。我們看一下這里的檢驗(yàn)連接可用性的代碼:

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

             //判斷usePingMethod是否為true
      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;
            }

          //使用該類中的ping類進(jìn)行執(zhí)行。這里的ping并不是執(zhí)行sql,而是ping命令
            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;
        }
    }

    //如果usePingMethod為false,那么就執(zhí)行sql
    String query = validateQuery;
     //這里的validateQuery,就是spring.datasource.druid.validation-query的配置項(xiàng),這里如果為空的話,就采用select 1
    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);
    }

}

}
那么這里的usePingMethod又是如何判斷的,按理說這里的值應(yīng)該是true,然后導(dǎo)致后邊的檢索庫的sql沒有進(jìn)行。也就導(dǎo)致獲取最近的接受mysql數(shù)據(jù)包的時間沒有更新。那么問題的核心又變成了usePingMethod的賦值問題。發(fā)現(xiàn)在初始化的時候就就行了usePingMethod的初始化

        public MySqlValidConnectionChecker(){
        try {
          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);
        }

        if (ping != null) {
            usePingMethod = true;
        }
    } catch (Exception e) {
        LOG.warn("Cannot resolve com.mysql.jdbc.Connection.ping method.  Will use 'SELECT 1' instead.", e);
    }

    configFromProperties(System.getProperties());
}

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

通過上述分析,我們大概明白了錯誤的原因,那么我們需要明白這個錯誤導(dǎo)致返回false,最后是否會對業(yè)務(wù)有什么影響。我們需要知道是誰在調(diào)這塊的代碼

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

        if (testOnBorrow) {
           //測試數(shù)據(jù)庫可用性
            boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);
            if (!validate) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("skip not validate connection.");
                }
        
                discardConnection(poolableConnection.holder);
                continue;
            }
        }

當(dāng)testConnectionInternal返回為false的時候,就會執(zhí)行這里的discardConnection方法,這里要釋放這個連接了,

  public void discardConnection(DruidConnectionHolder holder) {
    if (holder != null) {
        Connection conn = holder.getConnection();
        if (conn != null) {
          //這塊應(yīng)該是釋放的主要動作了。
            JdbcUtils.close(conn);
        }
  //多線程加鎖
        this.lock.lock();
        try {
            if (holder.discard) {
                return;
            }
            if (holder.active) {
            //讓活動的連接數(shù)減少一個
                --this.activeCount;
                holder.active = false;
            }
          //銷毀的個數(shù)+1
            ++this.discardCount;
            holder.discard = true;
      //如果活動的線程小于最小的連接數(shù)
            if (this.activeCount <= this.minIdle) {
      //這里釋放信號量,負(fù)責(zé)創(chuàng)建的線程會新建連接
                this.emptySignal();
            }
        } finally {
            this.lock.unlock();
        }
    }

JdbcUtils.close(conn);主要設(shè)置了一下判斷標(biāo)志

   public static void close(Connection x) {
    if (x != null) {
        try {
            if (x.isClosed()) {
                return;
            }
            x.close();
        } catch (Exception var2) {
            LOG.debug("close connection error", var2);
        }
    }
}

通過上述代碼,我們并沒有發(fā)現(xiàn)銷毀數(shù)組中連接的操作,那么我們看創(chuàng)建新鏈接的的時候有沒有相關(guān)的邏輯,方法如下:

  private void emptySignal() {
    if (this.createScheduler == null) {
        this.empty.signal();
    } else if (this.createTaskCount < this.maxCreateTaskCount) {
  //如果創(chuàng)建的連接小于最大的連接
        if (this.activeCount + this.poolingCount + this.createTaskCount < this.maxActive) {
     //正在活動的連接+連接池中剩下的+現(xiàn)在要創(chuàng)建的之<允許的最大的連接;
 // 就創(chuàng)建一個
            this.submitCreateTask(false);
        }
      }
}

druid connections[]數(shù)組

 DruidConnectionHolder takeLast() throws InterruptedException, SQLException {
      try {
        while(this.poolingCount == 0) {
          //如果連接池中連接數(shù)為0,就發(fā)信號,讓其創(chuàng)建的線程來創(chuàng)建
            this.emptySignal();
            if (this.failFast && this.isFailContinuous()) {
                throw new DataSourceNotAvailableException(this.createError);
            }
            ++this.notEmptyWaitThreadCount;
            if (this.notEmptyWaitThreadCount > this.notEmptyWaitThreadPeak) {
                this.notEmptyWaitThreadPeak = this.notEmptyWaitThreadCount;
            }
            try {
       //如果池子中為空,就阻塞。等待創(chuàng)建線程創(chuàng)建好了之后進(jìn)行喚醒
                this.notEmpty.await();
            } finally {
                --this.notEmptyWaitThreadCount;
            }
            ++this.notEmptyWaitCount;
            if (!this.enable) {
                connectErrorCountUpdater.incrementAndGet(this);
                if (this.disableException != null) {
                    throw this.disableException;
                }
                throw new DataSourceDisableException();
            }
        }
    } catch (InterruptedException var5) {
        this.notEmpty.signal();
        ++this.notEmptySignalCount;
        throw var5;
    }
       //將連接池中的連接減少一個,因?yàn)檫@個要出去干活了。
    this.decrementPoolingCount();
      //拿到這個連接
    DruidConnectionHolder last = this.connections[this.poolingCount];
      //設(shè)置為空,gc的時候進(jìn)行空間釋放
    this.connections[this.poolingCount] = null;
      //返回
    return last;

通過上述分析,我們知道獲取連接的時候是通過connects數(shù)組獲取的,獲取之后就交給業(yè)務(wù)。所以說上邊的釋放對連接池沒有任何影響,所以對業(yè)務(wù)沒有影響。通過上述分析,我們對druid數(shù)據(jù)庫連接池的工作過程有了很近一步的理解。至于上述的連接置為null的操作在線程池中也是相同的做法

對于druid連接池來說。報錯信息:

 ([com.alibaba.druid.pool.DruidAbstractDataSource:]) discard long time none received connection.

不會影響業(yè)務(wù)。避免報錯的方法是在項(xiàng)目啟動的時候通過腳本添加是否使用ping命令來檢測連接的可用性。druid讀取該配置的時候直接讀取的系統(tǒng)變量。所以在項(xiàng)目中添加配置是沒有作用的。當(dāng)上述報錯產(chǎn)生之后,druid會將連接銷毀,并嘗試從連接池中獲取新鏈接。如果沒有的話就會創(chuàng)建。其中的界限條件就是最小連接數(shù),最大連接數(shù)等。除此之外,druid有兩個線程,分別為連接創(chuàng)建線程和心跳檢測線程。他們相互配合保證連接的可用性和連接異步創(chuàng)建,所以對業(yè)務(wù)來說,總是有連接可用的

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容