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) {
  // 如果有拦截器  拦截器链 责任链模式执行所有的拦截
    FilterChainImpl filterChain = new FilterChainImpl(this);
    // 拦截器获取数据库连接代码详解 , 应该也是执行完拦截后获取连接
    return filterChain.dataSource_connect(this, maxWaitMillis);
} else {
   // 直接获取连接
    return getConnectionDirect(maxWaitMillis);
}

}

//直接获取连接

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) {
    // 测试连接
        boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);
        if (!validate) {
                //验证异常
            if (LOG.isDebugEnabled()) {
                LOG.debug("skip not validate connection.");
            }
            //关闭连接
            discardConnection(poolableConnection.holder);
            continue;
        }
    } else {
        if (poolableConnection.conn.isClosed()) {
            discardConnection(poolableConnection.holder); // 传入null,避免重复关闭
            continue;
        }
                    // todo 不太能理解 既然已经在init的时候已经启动了守护线程createAndStartDestroyThread去进行空闲线程和超时线程的定时扫描和回收,为什么在获取连接的时候还要重新处理一次呢???
        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) {
                //运行时间间隔设置为默认时间
                timeBetweenEvictionRunsMillis = DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS;
            }

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

druid中找到相关代码,查看报错相关的变量的逻辑

   if (validConnectionChecker != null) {
     //校验连接状态,其中的validationQuery就是我们要执行的sql语句,validationQueryTimeOut表示执行sql的超时时间,valid表示是否检验成功或者正常响应
            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
                
             //获取最近一次查询数据库接受数据包的时间
                long lastPacketReceivedTimeMs = MySqlUtils.getLastPacketReceivedTimeMs(conn);
                if (lastPacketReceivedTimeMs > 0) {
           
                //当前时间与最后一次接受数据的时间差
                    long mysqlIdleMillis = currentTimeMillis - lastPacketReceivedTimeMs;
                  //如果时间差大于druid的配置timeBetweenEvictionRunsMislis就开始报错。
                 //timeBetweenEvictionRunsMislis的默认时间是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的时间变大。于是我将这个值改大了再试试,结果还是在报错。
主要是lastPacketReceivedTimeMs 代表的是最后一次接受数据包的时间。显然这里的时间可以是昨天或者很久以前。那么为了控制让程序不要执行这里的报错和返回false,只能通过修改这里的valid了。我们看一下这里的检验连接可用性的代码:

         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类进行执行。这里的ping并不是执行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,那么就执行sql
    String query = validateQuery;
     //这里的validateQuery,就是spring.datasource.druid.validation-query的配置项,这里如果为空的话,就采用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又是如何判断的,按理说这里的值应该是true,然后导致后边的检索库的sql没有进行。也就导致获取最近的接受mysql数据包的时间没有更新。那么问题的核心又变成了usePingMethod的赋值问题。发现在初始化的时候就就行了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);
    }
}

通过上述分析,我们大概明白了错误的原因,那么我们需要明白这个错误导致返回false,最后是否会对业务有什么影响。我们需要知道是谁在调这块的代码

      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) {
           //测试数据库可用性
            boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);
            if (!validate) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("skip not validate connection.");
                }
        
                discardConnection(poolableConnection.holder);
                continue;
            }
        }

当testConnectionInternal返回为false的时候,就会执行这里的discardConnection方法,这里要释放这个连接了,

  public void discardConnection(DruidConnectionHolder holder) {
    if (holder != null) {
        Connection conn = holder.getConnection();
        if (conn != null) {
          //这块应该是释放的主要动作了。
            JdbcUtils.close(conn);
        }
  //多线程加锁
        this.lock.lock();
        try {
            if (holder.discard) {
                return;
            }
            if (holder.active) {
            //让活动的连接数减少一个
                --this.activeCount;
                holder.active = false;
            }
          //销毁的个数+1
            ++this.discardCount;
            holder.discard = true;
      //如果活动的线程小于最小的连接数
            if (this.activeCount <= this.minIdle) {
      //这里释放信号量,负责创建的线程会新建连接
                this.emptySignal();
            }
        } finally {
            this.lock.unlock();
        }
    }

JdbcUtils.close(conn);主要设置了一下判断标志

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

通过上述代码,我们并没有发现销毁数组中连接的操作,那么我们看创建新链接的的时候有没有相关的逻辑,方法如下:

  private void emptySignal() {
    if (this.createScheduler == null) {
        this.empty.signal();
    } else if (this.createTaskCount < this.maxCreateTaskCount) {
  //如果创建的连接小于最大的连接
        if (this.activeCount + this.poolingCount + this.createTaskCount < this.maxActive) {
     //正在活动的连接+连接池中剩下的+现在要创建的之<允许的最大的连接;
 // 就创建一个
            this.submitCreateTask(false);
        }
      }
}

druid connections[]数组

 DruidConnectionHolder takeLast() throws InterruptedException, SQLException {
      try {
        while(this.poolingCount == 0) {
          //如果连接池中连接数为0,就发信号,让其创建的线程来创建
            this.emptySignal();
            if (this.failFast && this.isFailContinuous()) {
                throw new DataSourceNotAvailableException(this.createError);
            }
            ++this.notEmptyWaitThreadCount;
            if (this.notEmptyWaitThreadCount > this.notEmptyWaitThreadPeak) {
                this.notEmptyWaitThreadPeak = this.notEmptyWaitThreadCount;
            }
            try {
       //如果池子中为空,就阻塞。等待创建线程创建好了之后进行唤醒
                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;
    }
       //将连接池中的连接减少一个,因为这个要出去干活了。
    this.decrementPoolingCount();
      //拿到这个连接
    DruidConnectionHolder last = this.connections[this.poolingCount];
      //设置为空,gc的时候进行空间释放
    this.connections[this.poolingCount] = null;
      //返回
    return last;

通过上述分析,我们知道获取连接的时候是通过connects数组获取的,获取之后就交给业务。所以说上边的释放对连接池没有任何影响,所以对业务没有影响。通过上述分析,我们对druid数据库连接池的工作过程有了很近一步的理解。至于上述的连接置为null的操作在线程池中也是相同的做法

对于druid连接池来说。报错信息:

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

不会影响业务。避免报错的方法是在项目启动的时候通过脚本添加是否使用ping命令来检测连接的可用性。druid读取该配置的时候直接读取的系统变量。所以在项目中添加配置是没有作用的。当上述报错产生之后,druid会将连接销毁,并尝试从连接池中获取新链接。如果没有的话就会创建。其中的界限条件就是最小连接数,最大连接数等。除此之外,druid有两个线程,分别为连接创建线程和心跳检测线程。他们相互配合保证连接的可用性和连接异步创建,所以对业务来说,总是有连接可用的

你可能感兴趣的:(Druid连接报错discard long time none received connection)