druid报错 discard long time none received connection

problem background

When the project started, there were a large number
insert image description here
c.a.d.pool.DruidAbstractDataSource: discard long time none received connection.
of database connections apparently managed by Druid because they had not received data from the database for too long, and the connections were recycled. This caused the service to prolong the service startup time because of the repeated creation of connections at startup.

positioning reason

According to the error message, find the Druid source code
com.alibaba.druid.pool.DruidAbstractDataSource#testConnectionInternal(com.alibaba.druid.pool.DruidConnectionHolder, java.sql.Connection)

if (validConnectionChecker != null) {
    
    
	// 验证连接的有效性 mysql下实际调用代码在下面那块
	boolean valid = validConnectionChecker.isValidConnection(conn, validationQuery, validationQueryTimeout);
	long currentTimeMillis = System.currentTimeMillis();
    if (holder != null) {
    
    
        holder.lastValidTimeMillis = currentTimeMillis;
        holder.lastExecTimeMillis = currentTimeMillis;
    }

    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
                        + ", version : " + VERSION.getVersionNumber()
                        + ", lastPacketReceivedIdleMillis : " + mysqlIdleMillis;
                LOG.warn(errorMsg);
                return false;
            }
        }
    }
    // ... 省略
}


// com.alibaba.druid.pool.vendor.MySqlValidConnectionChecker#isValidConnection
    public boolean isValidConnection(Connection conn, String validateQuery, int validationQueryTimeout) throws Exception {
    
    
        if (conn.isClosed()) {
    
    
            return false;
        }

        if (usePingMethod) {
    
    
        	// 以ping的方式检测连接的有效性
            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()) {
    
    
        	// 以 sql SELECT 1 的方式验证连接有效性
            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);
        }

    }
}

This is testConnectionInternalthe upper layer of the calling method.
insert image description here
It can be seen that because we have turned on testOnBorrowthe switch, the database connection will be tested immediately after the application is successful, and then according to the last heartbeat time of the database connection, it is judged whether it has been idle for too long and should be discarded. Database Connectivity.
This switch mainly checks the validity of the connection as soon as it is obtained from the connection pool.
If it is not enabled testOnBorrow, it will constantly check the idleness of the connection during the process of maintaining the connection, and recycle the connection that has been idle for too long.

com.alibaba.druid.util.MySqlUtils#getLastPacketReceivedTimeMsThis method returns the time when the connection last received a message.

// 以mysql6的 com.mysql.cj.jdbc.ConnectionImpl 为栗子
// getLastPacketReceivedTimeMs 方法中获取链接时间的实际方法
public long getIdleFor() {
    
    
     return this.lastQueryFinishedTime == 0 ? 0 : System.currentTimeMillis() - this.lastQueryFinishedTime;
 }

// com.mysql.cj.NativeSession#execSQL
    public <T extends Resultset> T execSQL(Query callingQuery, String query, int maxRows, NativePacketPayload packet, boolean streamResults,
            ProtocolEntityFactory<T, NativePacketPayload> resultSetFactory, ColumnDefinition cachedMetadata, boolean isBatch) {
    
    

        long queryStartTime = this.gatherPerfMetrics.getValue() ? System.currentTimeMillis() : 0;
        int endOfQueryPacketPosition = packet != null ? packet.getPosition() : 0;

        this.lastQueryFinishedTime = 0; // we're busy!

        if (this.autoReconnect.getValue() && (getServerSession().isAutoCommit() || this.autoReconnectForPools.getValue()) && this.needsPing && !isBatch) {
    
    
            try {
    
    
                ping(false, 0);
                this.needsPing = false;

            } catch (Exception Ex) {
    
    
                invokeReconnectListeners();
            }
        }

        try {
    
    
            return packet == null
                    ? ((NativeProtocol) this.protocol).sendQueryString(callingQuery, query, this.characterEncoding.getValue(), maxRows, streamResults,
                            cachedMetadata, resultSetFactory)
                    : ((NativeProtocol) this.protocol).sendQueryPacket(callingQuery, packet, maxRows, streamResults, cachedMetadata, resultSetFactory);

        } catch (CJException sqlE) {
    
    
            if (getPropertySet().getBooleanProperty(PropertyKey.dumpQueriesOnException).getValue()) {
    
    
                String extractedSql = NativePacketPayload.extractSqlFromPacket(query, packet, endOfQueryPacketPosition,
                        getPropertySet().getIntegerProperty(PropertyKey.maxQuerySizeToLog).getValue());
                StringBuilder messageBuf = new StringBuilder(extractedSql.length() + 32);
                messageBuf.append("\n\nQuery being executed when exception was thrown:\n");
                messageBuf.append(extractedSql);
                messageBuf.append("\n\n");
                sqlE.appendMessage(messageBuf.toString());
            }

            if ((this.autoReconnect.getValue())) {
    
    
                if (sqlE instanceof CJCommunicationsException) {
    
    
                    // IO may be dirty or damaged beyond repair, force close it.
                    this.protocol.getSocketConnection().forceClose();
                }
                this.needsPing = true;
            } else if (sqlE instanceof CJCommunicationsException) {
    
    
                invokeCleanupListeners(sqlE);
            }
            throw sqlE;

        } catch (Throwable ex) {
    
    
            if (this.autoReconnect.getValue()) {
    
    
                if (ex instanceof IOException) {
    
    
                    // IO may be dirty or damaged beyond repair, force close it.
                    this.protocol.getSocketConnection().forceClose();
                } else if (ex instanceof IOException) {
    
    
                    invokeCleanupListeners(ex);
                }
                this.needsPing = true;
            }
            throw ExceptionFactory.createException(ex.getMessage(), ex, this.exceptionInterceptor);

        } finally {
    
    
        	// 需要开启数据库连接的jdbc参数 maintainTimeStats=true
            if (this.maintainTimeStats.getValue()) {
    
    
            	// 连接的最后查询时间被更新
                this.lastQueryFinishedTime = System.currentTimeMillis();
            }

            if (this.gatherPerfMetrics.getValue()) {
    
    
                ((NativeProtocol) this.protocol).getMetricsHolder().registerQueryExecutionTime(System.currentTimeMillis() - queryStartTime);
            }
        }
    }

solve

Through source code analysis, the cause of the problem is roughly clear.
Druid will obtain a batch of connections from the database and hold them locally for quick use.
In order to check the availability of the connection (such as connection timeout being recycled by the database, network exception, etc.), when the testOnBorrowswitch is turned on, the idle connection check will be performed when the client obtains the connection from druid.
When idle checking, compare the difference between the current time of the connection and the time of the last SQL execution. Our service did not perform data query when it was started, and the connection is kept alive through ping. Therefore, when the startup time exceeds the previously set 15s, the detection fails when
the initial pooled database connection is used, and the beginning of the article is thrown. 借入exception information.

We can increase the idle connection removal time and keep-alive time so that idle connections can survive the no-data query time of service startup.
In addition, if the activity of the service is very low, that is, the frequency of executing sql is very low, you can set the environment variable druid.mysql.usePingMethodto falselet druid execute SELECT 1sql to keep the connection alive, so that getLastPacketReceivedTimeMsthe attributes will be refreshed incidentally.

// com.alibaba.druid.pool.vendor.MySqlValidConnectionChecker#configFromProperties
    public void configFromProperties(Properties properties) {
    
    
        if (properties == null) {
    
    
            return;
        }

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

Of course, there are other ways through the source code, which can be discovered by yourself.

spring:
	datasource:
		druid:
			# 让底层的jdbc维护连接的状态的时间
			url: jdck:mysql://xxx?maintainTimeStats=true
			# 连接闲置剔除时间
      		time-between-eviction-runs-millis: 300000
      		# 必须大于 time-between-eviction-runs-millis 时间
      		keep-alive-between-time-millis: 450000
	// 启动代码添加系统属性
	// 或者通过 -Ddruid.mysql.usePingMethod=false 的命令参数
	// 或者通过环境变量
    public static void main(String[] args) {
    
    
        Properties properties = System.getProperties();
        // 用 select 1 替换 ping 来检测连接保活
        properties.setProperty("druid.mysql.usePingMethod", "false");

        SpringApplication.run(App.class, args);
    }

Guess you like

Origin blog.csdn.net/weixin_46080554/article/details/129878573