深入剖析 Druid 连接池:连接有用性检测与 Keep-Alive 机制 ...

打印 上一主题 下一主题

主题 1897|帖子 1897|积分 5695

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
背景

在 Java 程序中,下面是一个经常会遇到的错误。
  1. Caused by: com.mysql.cj.exceptions.CJCommunicationsException: Communications link failure

  2. 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方法中实现的,下面我们看看该方法的具体实现细节。
  1. public DruidPooledConnection getConnectionDirect(long maxWaitMillis) throws SQLException {
  2.         int notFullTimeoutRetryCnt = 0;
  3.         for (; ; ) {
  4.             DruidPooledConnection poolableConnection;
  5.             try {
  6.                 // 从连接池中获取空闲连接
  7.                 poolableConnection = getConnectionInternal(maxWaitMillis);
  8.             } catch (GetConnectionTimeoutException ex) {
  9.                 ...
  10.             }
  11.             // 如果testOnBorrow为true,则会调用testConnectionInternal检测连接的有效性
  12.             if (testOnBorrow) {
  13.                 boolean validated = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);
  14.                 if (!validated) {
  15.                     if (LOG.isDebugEnabled()) {
  16.                         LOG.debug("skip not validated connection.");
  17.                     }
  18.                     // 如果连接无效,则会调用discardConnection丢弃该连接,并继续从连接池中获取新的空闲连接。
  19.                     discardConnection(poolableConnection.holder);
  20.                     continue;
  21.                 }
  22.             } else {
  23.                 ...
  24.                 // 如果testOnBorrow不为true,且testWhileIdle为true,则判断连接的空闲时间是否超过timeBetweenEvictionRunsMillis,如果超过,也会调用testConnectionInternal检测连接的有效性
  25.                 if (testWhileIdle) {
  26.                     final DruidConnectionHolder holder = poolableConnection.holder;
  27.                     long currentTimeMillis = System.currentTimeMillis();
  28.                     long lastActiveTimeMillis = holder.lastActiveTimeMillis;
  29.                     ...
  30.                     long idleMillis = currentTimeMillis - lastActiveTimeMillis;

  31.                     if (idleMillis >= timeBetweenEvictionRunsMillis
  32.                             || idleMillis < 0 // unexcepted branch
  33.                     ) {
  34.                         boolean validated = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);
  35.                         if (!validated) {
  36.                             if (LOG.isDebugEnabled()) {
  37.                                 LOG.debug("skip not validated connection.");
  38.                             }
  39.                             // 如果连接无效,则会调用discardConnection丢弃该连接,并继续从连接池中获取新的空闲连接。
  40.                             discardConnection(poolableConnection.holder);
  41.                             continue;
  42.                         }
  43.                     }
  44.                 }
  45.             }
  46.             ...
  47.             return poolableConnection;
  48.         }
  49.     }
复制代码
该方法的处理流程如下:

  • 通过 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方法的具体实现。
  1. // DruidDataSource.java
  2. protected void recycle(DruidPooledConnection pooledConnection) throws SQLException {
  3.         final DruidConnectionHolder holder = pooledConnection.holder;
  4.             ...
  5.             if (testOnReturn) {
  6.                 boolean validated = testConnectionInternal(holder, physicalConnection);
  7.                 if (!validated) {
  8.                     JdbcUtils.close(physicalConnection);
  9.                     ...
  10.                 }
  11.             }
  12.             ...
  13.     }
复制代码
方法中的 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 剖析和执行的开销,提高性能。
  1. public PhysicalConnectionInfo createPhysicalConnection() throws SQLException {
  2.         String url = this.getUrl();
  3.         Properties connectProperties = getConnectProperties();
  4.         ...
  5.         try {
  6.             // 这里会调用驱动的 connect 方法来建立连接
  7.             conn = createPhysicalConnection(url, physicalConnectProperties);
  8.             connectedNanos = System.nanoTime();

  9.             if (conn == null) {
  10.                 throw new SQLException("connect error, url " + url + ", driverClass " + this.driverClass);
  11.             }
  12.             ...
  13.             if (!initSqls(conn, variables, globalVariables)) {
  14.                 validateConnection(conn);
  15.             }
  16.             ...
  17.         } 
  18.         ...
  19.         return new PhysicalConnectionInfo(conn, connectStartNanos, connectedNanos, initedNanos, validatedNanos, variables, globalVariables);
  20.     }
复制代码
关于参数设置的几点建议


  • 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企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

东湖之滨

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表