马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
x
背景
在 Java 程序中,下面是一个经常会遇到的错误。- Caused by: com.mysql.cj.exceptions.CJCommunicationsException: Communications link failure
- The last packet successfully received from the server was 30,027 milliseconds ago. The last packet sent successfully to the server was 30,028 milliseconds ago.
复制代码 该错误通常是由于 MySQL 连接意外断开导致的,常见原因包括:
- 客户端连接池(如 HikariCP、Druid)配置不当,包括:
- 空闲连接超时时间超过 MySQL wait_timeout(默认是 28800 秒,即 8 小时),导致连接被 MySQL 服务端关闭。
- 未配置适当的 Keep-Alive 机制,导致连接长时间未利用而被 MySQL 服务器关闭。
- 未进行连接有用性检查,大概导致客户端获取到失效连接。
- 连接未及时释放。
长时间未释放的连接无法通过连接池的 Keep-Alive 机制保持活泼,更容易因空闲超时被 MySQL 服务端或中间件关闭。
- 中间层组件的超时限定。
如果客户端与 MySQL 之间存在代理(如 ProxySQL)或负载均衡器(LB),这些组件大概会有独立的空闲连接超时设置,导致连接被提前断开。
- 网络标题,包括高耽误、丢包或短暂网络中断都会影响数据库连接的稳固性。
- 连接被 MySQL 服务器自动断开,如 DBA 手动执行KILL操作终止连接。
本文将深入剖析 Druid 连接池的连接有用性检测机制,重点探讨以下内容:
- Druid 在哪些情况下会检查连接是否可用?
- Druid 怎样保持连接的活泼状态(Keep-Alive 机制)?
- Druid 连接池中常见参数的具体含义及其作用。
- 为什么 MySQL 的 general log 看不到validationQuery界说的检测语句执行?
盼望通过本篇分析,帮助各人更深入理解 Druid 连接池的运行机制。
什么场景下会检测连接的有用性
Druid 连接池在以下四种场景下会检测连接的有用性:
- 申请连接。
- 归还连接。
- 创建新的物理连接。
- 定期检测。
下面我们看看这四种场景的具体实现逻辑。
1. 申请连接
当应用从连接池申请空闲连接时,会检查连接的有用性,与之相关的参数有两个:testOnBorrow 和 testWhileIdle。
申请连接是在getConnectionDirect方法中实现的,下面我们看看该方法的具体实现细节。- public DruidPooledConnection getConnectionDirect(long maxWaitMillis) throws SQLException {
- int notFullTimeoutRetryCnt = 0;
- for (; ; ) {
- DruidPooledConnection poolableConnection;
- try {
- // 从连接池中获取空闲连接
- poolableConnection = getConnectionInternal(maxWaitMillis);
- } catch (GetConnectionTimeoutException ex) {
- ...
- }
- // 如果testOnBorrow为true,则会调用testConnectionInternal检测连接的有效性
- if (testOnBorrow) {
- boolean validated = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);
- if (!validated) {
- if (LOG.isDebugEnabled()) {
- LOG.debug("skip not validated connection.");
- }
- // 如果连接无效,则会调用discardConnection丢弃该连接,并继续从连接池中获取新的空闲连接。
- discardConnection(poolableConnection.holder);
- continue;
- }
- } else {
- ...
- // 如果testOnBorrow不为true,且testWhileIdle为true,则判断连接的空闲时间是否超过timeBetweenEvictionRunsMillis,如果超过,也会调用testConnectionInternal检测连接的有效性
- if (testWhileIdle) {
- final DruidConnectionHolder holder = poolableConnection.holder;
- long currentTimeMillis = System.currentTimeMillis();
- long lastActiveTimeMillis = holder.lastActiveTimeMillis;
- ...
- long idleMillis = currentTimeMillis - lastActiveTimeMillis;
- if (idleMillis >= timeBetweenEvictionRunsMillis
- || idleMillis < 0 // unexcepted branch
- ) {
- boolean validated = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);
- if (!validated) {
- if (LOG.isDebugEnabled()) {
- LOG.debug("skip not validated connection.");
- }
- // 如果连接无效,则会调用discardConnection丢弃该连接,并继续从连接池中获取新的空闲连接。
- discardConnection(poolableConnection.holder);
- continue;
- }
- }
- }
- }
- ...
- return poolableConnection;
- }
- }
复制代码 该方法的处理流程如下:
- 通过 checkCount = poolingCount - minIdle 盘算当前池中可以回收的连接数目。其中minIdle是 Druid 参数,用于指定连接池需保留的最小空闲连接数。
- 遍历连接池中的连接,并执行以下检查:
- 物理连接存活时间检查:如果 phyTimeoutMillis > 0,检查物理连接的存活时长是否超过phyTimeoutMillis,如果超过,则将该连接加入烧毁列表(evictConnections)。
- 空闲时间检查:如果连接的空闲时间大于 maxEvictableIdleTimeMillis,或空闲时间大于等于 minEvictableIdleTimeMillis 且连接的序号小于 checkCount(可回收连接数),则将该连接加入烧毁列表。
- 保持连接活泼:若连接必要保持活泼(keepAlive开启)且空闲时间超过keepAliveBetweenTimeMillis,则将该连接加入 keepAliveConnections 列表。
- 将未被检查的连接移动到 remaining 之后的位置,确保有用连接的一连性。
- 如果 evictCount 大于 0,表现有连接必要烧毁,遍历烧毁列表(evictConnections)关闭这些连接。
- 如果 keepAliveCount 大于 0,表现有连接必要保持活泼,遍历 keepAliveConnections 列表,检查连接有用性,若有用,将其放回连接池。如果校验失败,则丢弃连接。
- 如果 needFill 为 true,表现连接池中空闲连接不足,触发填充信号以创建新连接。
以是,Druid 连接池默认情况下,每 60 秒(由 timeBetweenEvictionRunsMillis 参数控制)执行一次连接回收和维护操作,并保持肯定数目的空闲连接。其核心逻辑包括:
- 回收超时或多余的空闲连接:
- 连接的空闲时间超过 maxEvictableIdleTimeMillis 或 phyConnectTimeMillis,将被回收。
- 当连接池的数目超过最小空闲连接数 minIdle 时,如果连接的空闲时间超过 minEvictableIdleTimeMillis,也会被回收。
- 维护 Keep-Alive 机制(如果keepAlive开启):
- 当连接的空闲时间超过 keepAliveBetweenTimeMillis,且距离上次 Keep-Alive 检测时间超过 keepAliveBetweenTimeMillis 时,执行有用性检测。
- 通过 validateConnection 进行检测,合格的连接重新加入池中,不合格的连接被烧毁。
- 必要时补充新的连接:
若当前连接数(activeCount + poolingCount)低于 minIdle,则触发连接补充机制,创建新的连接。
必要注意的是,即使连接开启了定期探活检测,若发生超时,仍会被回收。
接下来,我们看看上述参数的默认值:
- timeBetweenEvictionRunsMillis:默认 60000 毫秒(60 秒)。
- minEvictableIdleTimeMillis:默认 1800000 毫秒(30 分钟)。
- maxEvictableIdleTimeMillis:默认 25200000 毫秒(7 小时)。
- phyTimeoutMillis:默认 -1。
- keepAlive:默认为 false。
- keepAliveBetweenTimeMillis:默认 120000 毫秒(120 秒)。
为什么设置的 validationQuery 没有用果?
在 Druid 连接池中,判定连接是否有用时,通常调用 testConnectionInternal 或 validateConnection 方法。这两个方法的核心逻辑根本雷同,具体如下:
- 优先利用 validConnectionChecker 进行连接校验:
- validConnectionChecker 是一个接口,界说了 isValidConnection 方法,用于检测数据库连接的有用性。
- 具体的数据库有对应的实现类,例如:MySQL 由MySqlValidConnectionChecker实现,Oracle 由 OracleValidConnectionChecker 实现。
- validConnectionChecker 在 initValidConnectionChecker 方法中初始化,并根据数据库驱动类型选择合适的实现类。
- 如果 validConnectionChecker 未初始化,则执行默认检查:
- 通过 validationQuery 执行 SQL 语句,验证连接是否有用。
- 该方法适用于全部数据库,但会带来肯定的性能开销。
以下是 MySQL 实现类(MySqlValidConnectionChecker)中isValidConnection方法的具体实现。- // DruidDataSource.java
- protected void recycle(DruidPooledConnection pooledConnection) throws SQLException {
- final DruidConnectionHolder holder = pooledConnection.holder;
- ...
- if (testOnReturn) {
- boolean validated = testConnectionInternal(holder, physicalConnection);
- if (!validated) {
- JdbcUtils.close(physicalConnection);
- ...
- }
- }
- ...
- }
复制代码 方法中的 usePingMethod 受druid.mysql.usePingMethod参数控制,其默认值为 true。
当 usePingMethod 等于 true 时,validateQuery 将被设置为 DEFAULT_VALIDATION_QUERY,即/* ping */ SELECT 1,而非用户自界说的 validationQuery。
execValidQuery() 方法执行 validateQuery 时,如果查询语句以/* ping */开头,MySQL JDBC 驱动会进行特殊处理。
具体来说,MySQL JDBC 在剖析 SQL 语句时,会判定它是否以 PING_MARKER(即/* ping */)开头,如果是,则不会执行 SQL 语句,而是调用doPingInstead(),直接向 MySQL 服务器发送 COM_PING 命令,这样可以减少 SQL 剖析和执行的开销,提高性能。- public PhysicalConnectionInfo createPhysicalConnection() throws SQLException {
- String url = this.getUrl();
- Properties connectProperties = getConnectProperties();
- ...
- try {
- // 这里会调用驱动的 connect 方法来建立连接
- conn = createPhysicalConnection(url, physicalConnectProperties);
- connectedNanos = System.nanoTime();
- if (conn == null) {
- throw new SQLException("connect error, url " + url + ", driverClass " + this.driverClass);
- }
- ...
- if (!initSqls(conn, variables, globalVariables)) {
- validateConnection(conn);
- }
- ...
- }
- ...
- return new PhysicalConnectionInfo(conn, connectStartNanos, connectedNanos, initedNanos, validatedNanos, variables, globalVariables);
- }
复制代码 关于参数设置的几点建议
- minEvictableIdleTimeMillis,maxEvictableIdleTimeMillis不宜设置过小,因为频繁烧毁和创建连接会带来额外的性能开销。
- 建议开启 keepAlive 机制,尤其是在客户端与 MySQL 之间存在代理的情况下,这些组件大概会有独立的空闲连接超时设置,导致连接被提前断开。
- 在连接申请时检测连接的有用性(通过设置 testOnBorrow 为 true)是最有用的方式,可以确保每次获取的连接都是可用的。但这种方式会对应用性能产生肯定影响,尤其是在高并发场景下。
因此,建议根据业务需求权衡性能与可靠性,选择合适的检测计谋。
- 考虑到网络大概的故障,即使 Druid 连接池定期检测连接的有用性,也无法 100% 保证全部连接都可用,以是应用端肯定要做好容错处理。
- 对于代码中未及时归还利用过的连接,一方面大概导致连接泄漏,使连接池耗尽可用连接。另一方面,未释放的连接无法通过 Druid 的 Keep-Alive 机制保持活泼状态,更容易因空闲超时被 MySQL 服务器或中间件关闭。
为了避免这些标题,建议在应用的测试环境开启以下参数来辨认长时间未归还的连接:logAbandoned,removeAbandoned,removeAbandonedTimeoutMillis。
总结
- Druid 连接池在以下四种场景下会检测连接的有用性:申请连接、归还连接、创建新物理连接以及定期检测。
- Druid 通过开启 keepAlive 参数,定期对空闲连接进行有用性检测,确保连接保持活泼状态。
当连接的空闲时间超过 keepAliveBetweenTimeMillis 时,Druid 会触发 Keep-Alive 检测,验证连接的有用性。如果连接有用,则重新放回连接池;如果无效,则将其烧毁。
- Druid 默认利用 MySQL 的 COM_PING 命令进行连接有用性检测,这种方式比执行 SQL 语句更高效。
由于 COM_PING 的优先级高于用户自界说的 validationQuery,因此在默认配置下,validationQuery 不会被执行。
如果用户盼望利用自界说的 validationQuery 进行连接检测,可将 druid.mysql.usePingMethod 参数设置为 false 来实现。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |