在回答这个问题之前,首先我们看看 MySQL 中有哪些常用的 JDBC 连接池:

  • c3p0
  • DBCP
  • Druid
  • Tomcat JDBC Pool
  • HikariCP

这些连接池中,c3p0 是一个老牌的连接池,很多流行框架,在其老版本中,都将 c3p0 作为默认的连接池。

DBCP 和 Tomcat JDBC Pool(Tomcat 的默认连接池)是 Apache 开源的。

Druid 是阿里开源的,它不仅仅是个数据库连接池,还可以监控数据库的访问性能,支持数据库密码加密等。

HikariCP 是目前风头最劲的 JDBC 连接池,其号称性能最好。

从下图 HikariCP 官网给出的压测结果来看,也确实如此,性能上吊打 c3p0、DBCP2。

包括 SpringBoot 2.0 也将 HikariCP 作为默认的数据库连接池。

MySQL JDBC连接池中最高效的连接检测语句

实际上,对于这个问题,c3p0 的官方文档(https://www.mchange.com/projects/c3p0/)中给出了答案。

When configuring Connection testing, first try to minimize the cost of each test. If you are using a JDBC driver that you are certain supports the new(ish) jdbc4 API — and if you are using c3p0-0.9.5 or higher! — let your driver handle this for you. jdbc4 Connections include a method called isValid() that should be implemented as a fast, reliable Connection test. By default, c3p0 will use that method if it is present.

However, if your driver does not support this new-ish API, c3p0's default behavior is to test Connections by calling the getTables() method on a Connection's associated DatabaseMetaData object. This has the advantage of being very robust and working with any database, regardless of the database schema. However, a call to DatabaseMetaData.getTables() is often much slower than a simple database query, and using this test may significantly impair your pool's performance.

The simplest way to speed up Connection testing under a JDBC 3 driver (or a pre-0.9.5 version of c3p0) is to define a test query with the preferredTestQuery parameter. Be careful, however. Setting preferredTestQuery will lead to errors as Connection tests fail if the query target table does not exist in your database prior to initialization of your DataSource. Depending on your database and JDBC driver, a table-independent query like SELECT 1 may (or may not) be sufficient to verify the Connection. If a table-independent query is not sufficient, instead of preferredTestQuery, you can set the parameter automaticTestTable. Using the name you provide, c3p0 will create an empty table, and make a simple query against it to test the database.

从上面的描述中可以看到,最高效的连接检测语句是 JDBC4 中引入的isValid方法 。

其次是通过 preferredTestQuery 设置一个简单的查询操作(例如SELECT 1),最后才是默认的getTables方法。

包括 HikariCP 的文档中,也推荐使用 isValid 方法。只有当驱动比较老,不支持 isValid 方法时,才建议通过 connectionTestQuery 自定义检测语句。

connectionTestQuery

If your driver supports JDBC4 we strongly recommend not setting this property. This is for "legacy" drivers that do not support the JDBC4 Connection.isValid() API. This is the query that will be executed just before a connection is given to you from the pool to validate that the connection to the database is still alive. Again, try running the pool without this property, HikariCP will log an error if your driver is not JDBC4 compliant to let you know. Default: none

所以接下来,我们要弄懂以下几个问题:

  1. 什么是 isValid() 。

  2. 什么是 getTables()。

  3. 不同连接检测语句之间的性能对比情况。

  4. 为什么 isValid() 的性能最好,MySQL 服务端是如何处理的。

  5. 怎么设置才能使用 isValid() 进行连接检测?

什么是 isValid()

isValid 方法是在 JDBC4 中引入的。JDBC 是 Java 用于与关系型数据库通信的标准API。

JDBC4 指的是 Java Database Connectivity (JDBC) 的第 4 版本,该版本是在 Java 6(也被称为Java 1.6)中引入的。

所以只要程序使用的是 Java 1.6 及以上的版本,都支持 isValid 方法。

下面,我们看看这个方法的具体实现细节。

// src/main/user-impl/java/com/mysql/cj/jdbc/ConnectionImpl.java
@Override
public boolean isValid(int timeout) throws SQLException {
    synchronized (getConnectionMutex()) { // 获取与连接相关的锁
        if (isClosed()) {
            return false; // 如果连接已关闭,返回 false,表示连接无效
        }
        try {
            try {
                // 调用 pingInternal 方法,检查连接是否有效,timeout 参数以毫秒为单位
                pingInternal(false, timeout * 1000); 
            } catch (Throwable t) {
                try {
                    abortInternal(); // 如果 pingInternal 抛出异常,调用 abortInternal 方法中止连接
                } catch (Throwable ignoreThrown) { 
                    // we're dead now anyway // 忽略异常,因为连接已经无效
                }
                return false; // 返回 false,表示连接无效
            }
        } catch (Throwable t) {
            return false; // 如果在 try 块中的任何地方发生异常,返回 false,表示连接无效
        }
        return true; // 如果没有异常发生,表示连接有效,返回 true
    }
}

isValid 方法的核心是 pingInternal 方法。我们继续看看 pingInternal 方法的实现。

@Override
public void pingInternal(boolean checkForClosedConnection, int timeoutMillis) throws SQLException {
    this.session.ping(checkForClosedConnection, timeoutMillis);
}

方法中的 session 是个 NativeSession 对象。

以下是 NativeSession 类中 ping 方法的实现。

// src/main/core-impl/java/com/mysql/cj/NativeSession.java
public void ping(boolean checkForClosedConnection, int timeoutMillis) {
    if (checkForClosedConnection) { // 如果需要检查连接是否已关闭,调用 checkClosed 方法
        checkClosed();
    }
    ...
    // this.protocol.sendCommand 是发送命令,this.commandBuilder.buildComPing(null)是构造命令。
    this.protocol.sendCommand(this.commandBuilder.buildComPing(null), false, timeoutMillis); // it isn't safe to use a shared packet here
}

实现中的重点是 this.protocol.sendCommand 和 this.commandBuilder.buildComPing 这两个方法。前者用来发送命令,后者用来构造命令。

后者中的 commandBuilder 是个 NativeMessageBuilder 对象。

以下是 NativeMessageBuilder 类中 buildComPing 方法的实现。

// src/main/protocol-impl/java/com/mysql/cj/protocol/a/NativeMessageBuilder.java
public NativePacketPayload buildComPing(NativePacketPayload sharedPacket) {
    NativePacketPayload packet = sharedPacket != null ? sharedPacket : new NativePacketPayload(1);
    packet.writeInteger(IntegerDataType.INT1, NativeConstants.COM_PING);
    return packet;
}

NativePacketPayload 是与 MySQL 服务器通信的数据包,NativeConstants.COM_PING 即 MySQL 中的 COM_PING 命令包。

所以,实际上,isValid 方法封装的就是COM_PING命令包。

什么是 getTables()

getTables() 是 MySQL JDBC 驱动中 DatabaseMetaData 类中的一个方法,用来查询给定的库中是否有指定的表。

c3p0 使用这个方法检测时,只指定了表名 PROBABLYNOT。

库名因为设置的是 null,所以默认会使用 JDBC URL 中指定的数据库。如jdbc:mysql://10.0.0.198:3306/information_schema中的 information_schema。

{
    rs = c.getMetaData().getTables( null,
                                    null,
                                    "PROBABLYNOT",
                                    new String[] {"TABLE"} );
    return CONNECTION_IS_OKAY;
}

如果使用的驱动是 8.0 之前的版本,对应的检测语句是:

SHOW FULL TABLES FROM `information_schema` LIKE 'PROBABLYNOT'

如果使用的驱动是 8.0 之后的版本,对应的检测语句是:

SELECT TABLE_SCHEMA AS TABLE_CAT, NULL AS TABLE_SCHEM, TABLE_NAME, CASE WHEN TABLE_TYPE='BASE TABLE' THEN CASE WHEN TABLE_SCHEMA = 'mysql' OR TABLE_SCHEMA = 'performance_schema' THEN 'SYSTEM TABLE' ELSE 'TABLE' END WHEN TABLE_TYPE='TEMPORARY' THEN 'LOCAL_TEMPORARY' ELSE TABLE_TYPE END AS TABLE_TYPE, TABLE_COMMENT AS REMARKS, NULL AS TYPE_CAT, NULL AS TYPE_SCHEM, NULL AS TYPE_NAME, NULL AS SELF_REFERENCING_COL_NAME, NULL AS REF_GENERATION FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'PROBABLYNOT' HAVING TABLE_TYPE IN ('TABLE',null,null,null,null) ORDER BY TABLE_TYPE, TABLE_SCHEMA, TABLE_NAME

因为 c3p0 是一个老牌的连接池,它流行的时候 MySQL 8.0 还没发布。所以,对于 c3p0,见到更多的是前面这一个检测语句。

四个连接检测语句之间的性能对比

下面我们对比下PINGSELECT 1SHOW FULL TABLES FROM information_schema LIKE 'PROBABLYNOT' 和 INFORMATION_SCHEMA.TABLES这四个连接检测语句的执行耗时情况。

下面是具体的测试结果,每个语句循环执行了 100000 次。

PING time for 100000 iterations: 3.04852 seconds
SELECT 1 time for 100000 iterations: 12.61825 seconds
SHOW FULL TABLES time for 100000 iterations: 66.21564 seconds
INFORMATION_SCHEMA.TABLES time for 100000 iterations: 69.32230 seconds

为了避免网络的影响,测试使用的是 socket 连接。

测试脚本地址:https://github.com/slowtech/dba-toolkit/blob/master/mysql/connection_test_benchemark.py

测试脚本中,使用的是connection.ping(reconnect=False)命令。

这个 ping 命令跟 COM_PING 命令包有什么关系呢?

实际上,在 pymysql 中,ping 命令封装的就是 COM_PING 命令包。

def ping(self, reconnect=True):
    if self._sock is None:
        if reconnect:
            self.connect()
            reconnect = False
        else:
            raise err.Error("Already closed")
    try:
        self._execute_command(COMMAND.COM_PING, "")
        self._read_ok_packet()
    except Exception:
        if reconnect:
            self.connect()
            self.ping(False)
        else:
            raise

MySQL 服务端对于 COM_PING 的处理逻辑

以下是 MySQL 服务端处理 ping 命令的堆栈信息。

[mysqld] my_ok(THD *, unsigned long long, unsigned long long, const char *) sql_class.cc:3247
[mysqld] dispatch_command(THD *, const COM_DATA *, enum_server_command) sql_parse.cc:2268
[mysqld] do_command(THD *) sql_parse.cc:1362
[mysqld] handle_connection(void *) connection_handler_per_thread.cc:302
[mysqld] pfs_spawn_thread(void *) pfs.cc:2942
[libsystem_pthread.dylib] _pthread_start 0x0000000197d3bfa8

整个链路比较短,MySQL 服务端在接受到客户端请求后,首先会初始化一个线程。

线程初始化完毕后,会验证客户端用户的账号密码是否正确。

如果正确,则线程会循环从客户端连接中读取命令并执行。

不同的命令,会有不同的处理逻辑。

具体如何处理是在dispatch_command中定义的。

bool dispatch_command(THD *thd, const COM_DATA *com_data,
                      enum enum_server_command command) {
  ...
  switch (command) {
    case COM_INIT_DB: {
      LEX_STRING tmp;
      thd->status_var.com_stat[SQLCOM_CHANGE_DB]++;
      thd->convert_string(&tmp, system_charset_info,
                          com_data->com_init_db.db_name,
                          com_data->com_init_db.length, thd->charset());

      LEX_CSTRING tmp_cstr = {tmp.str, tmp.length};
      if (!mysql_change_db(thd, tmp_cstr, false)) {
        query_logger.general_log_write(thd, command, thd->db().str,
                                       thd->db().length);
        my_ok(thd);
      }
      break;
    }
    case COM_REGISTER_SLAVE: {
      // TODO: access of protocol_classic should be removed
      if (!register_replica(thd, thd->get_protocol_classic()->get_raw_packet(),
                            thd->get_protocol_classic()->get_packet_length()))
        my_ok(thd);
      break;
    }
    case COM_RESET_CONNECTION: {
      thd->status_var.com_other++;
      thd->cleanup_connection();
      my_ok(thd);
      break;
    }
    ...
    case COM_PING:
      thd->status_var.com_other++;
      my_ok(thd);  // Tell client we are alive
      break;
    ...
}

可以看到,对于 COM_PING 命令包,MySQL 服务端的处理比较简单,只是将 com_other(com_other 对应状态变量中的 Com_admin_commands)的值加 1,然后返回一个 OK 包。

反观SELECT 1命令,虽然看上去也足够简单,但毕竟是一个查询,是查询就要经过词法分析、语法分析、优化器、执行器等阶段。

以下是SELECT 1在执行阶段的堆栈信息,可以看到,它比 COM_PING 的堆栈信息要复杂不少。

[mysqld] FakeSingleRowIterator::Read() basic_row_iterators.h:284
[mysqld] Query_expression::ExecuteIteratorQuery(THD *) sql_union.cc:1290
[mysqld] Query_expression::execute(THD *) sql_union.cc:1343
[mysqld] Sql_cmd_dml::execute_inner(THD *) sql_select.cc:786
[mysqld] Sql_cmd_dml::execute(THD *) sql_select.cc:586
[mysqld] mysql_execute_command(THD *, bool) sql_parse.cc:4604
[mysqld] dispatch_sql_command(THD *, Parser_state *) sql_parse.cc:5239
[mysqld] dispatch_command(THD *, const COM_DATA *, enum_server_command) sql_parse.cc:1959
[mysqld] do_command(THD *) sql_parse.cc:1362
[mysqld] handle_connection(void *) connection_handler_per_thread.cc:302
[mysqld] pfs_spawn_thread(void *) pfs.cc:2942
[libsystem_pthread.dylib] _pthread_start 0x0000000181d53fa8

怎么设置才能使用 isValid() 进行连接检测

对于 HikariCP 连接池来说,不设置 connectionTestQuery 即可。

这一点,可从连接检测任务(KeepaliveTask)的代码中看出来。

try {
   final var validationSeconds = (int) Math.max(1000L, validationTimeout) / 1000;

   if (isUseJdbc4Validation) { 
      return !connection.isValid(validationSeconds);
   }

   try (var statement = connection.createStatement()) {
      if (isNetworkTimeoutSupported != TRUE) {
         setQueryTimeout(statement, validationSeconds);
      }

      statement.execute(config.getConnectionTestQuery());
   }
}

可以看到,是否使用 connection.isValid 是由 isUseJdbc4Validation 决定的。

而 isUseJdbc4Validation 是否为 true,是由配置中是否设置了connectionTestQuery决定的,该参数不设置则默认为 none。

this.isUseJdbc4Validation = config.getConnectionTestQuery() == null;

对于 c3p0 连接池来说,需使用 v0.9.5 及之后的版本,同时不要设置 preferredTestQuery 或者 automaticTestTable

注意,如果要定期对空闲连接进行检测,在 HikariCP 中,需要设置 keepaliveTime。而在 c3p0 中,则需设置 idleConnectionTestPeriod。这两个参数的默认值为 0,即不会对空闲连接进行定期检测。

mysqladmin ping 命令的实现逻辑

mysqladmin 中有个 ping 命令可以检查 MySQL 服务端的存活情况。

# mysqladmin --help | grep ping
  ping   Check if mysqld is alive
  
# mysqladmin -h127.0.0.1 -P3306 -uroot -p'123456' ping
mysqld is alive

这个 ping 命令实际上封装的就是 COM_PING 命令包。

// mysqladmin.cc
case ADMIN_PING:
  mysql->reconnect = false; /* We want to know of reconnects */
  if (!mysql_ping(mysql)) {
    if (option_silent < 2) puts("mysqld is alive");
  } else {
    if (mysql_errno(mysql) == CR_SERVER_GONE_ERROR) {
      mysql->reconnect = true;
      if (!mysql_ping(mysql))
        puts("connection was down, but mysqld is now alive");
    } else {
      my_printf_error(0, "mysqld doesn't answer to ping, error: '%s'",
                      error_flags, mysql_error(mysql));
      return -1;
    }
  }
  mysql->reconnect = true; /* Automatic reconnect is default */
  break;

// libmysql/libmysql.cc
int STDCALL mysql_ping(MYSQL *mysql) {
  int res;
  DBUG_TRACE;
  res = simple_command(mysql, COM_PING, nullptr, 0, 0);
  if (res == CR_SERVER_LOST && mysql->reconnect)
    res = simple_command(mysql, COM_PING, nullptr, 0, 0);
  return res;
}  

总结

  1. 连接检测语句,首选是 JDBC 驱动中的 isValid 方法,其次才是自定义查询语句。

  2. 虽然 isValid 方法是 JDBC 4 中才支持的,但 JDBC 4 早在 Java 6 中就引入了,所以,只要程序使用的是 Java 1.6 及以上的版本,都支持 isValid 方法。

  3. 从连接池性能的角度出发,不建议使用 c3p0。但有些框架,在比较老的版本中,还是将 c3p0 作为默认的连接池。

    如果使用的是 c3p0 v0.9.5 之前的版本,建议配置 preferredTestQuery。如果使用的是 v0.9.5 及之后的版本,推荐使用 isValid。

  4. SHOW FULL TABLES FROM xx LIKE 'PROBABLYNOT' 这个查询,在并发量较高的情况下,会对 MySQL 的性能产生较大的负面影响,线上慎用。

参考

  1. MySQL JDBC驱动地址:https://github.com/mysql/mysql-connector-j
  2. PyMySQL项目地址:https://github.com/PyMySQL/PyMySQL

什么是 MySQL JDBC 连接池中最高效的连接检测语句?的更多相关文章

  1. 网络协议 finally{ return问题 注入问题 jdbc注册驱动问题 PreparedStatement 连接池目的 1.2.1DBCP连接池 C3P0连接池 MYSQL两种方式进行实物管理 JDBC事务 DBUtils事务 ThreadLocal 事务特性 并发访问 隔离级别

    1.1.1 API详解:注册驱动 DriverManager.registerDriver(new com.mysql.jdbc.Driver());不建议使用 原因有2个: >导致驱动被注册2 ...

  2. 连接池中的maxIdle,MaxActive,maxWait等参数详解

    转: 连接池中的maxIdle,MaxActive,maxWait等参数详解 2017年06月03日 15:16:22 阿祥小王子 阅读数:6481   版权声明:本文为博主原创文章,未经博主允许不得 ...

  3. WebSphere中数据源连接池太小导致的连接超时错误记录

    WebSphere中数据源连接池太小导致的连接超时错误记录. 应用连接超时错误信息: [// ::: CST] webapp E com.ibm.ws.webcontainer.webapp.WebA ...

  4. 连接池中的maxIdle,MaxActive,maxWait参数

    连接池中的maxIdle,MaxActive,maxWait参数 线程池 name:表示你的连接池的名称也就是你要访问连接池的地址 auth:是连接池管理权属性,Container表示容器管理 typ ...

  5. jdbc连接池中c3p0的配置文件的详解以及在在java中如何使用

    <c3p0-config> <!-- 默认配置,如果没有指定则使用这个配置 --> <default-config> <property name=" ...

  6. getSessionFactory().openSession()导致druid连接池中的连接都占用满但无法回收

    该问题产生的现象 页面刷新几次后,就卡住,线上就得需要重新部署(还好是测试环境,不是真正生产环境) 过程及原因 查看日志线程池满了 Caused by: org.springframework.jdb ...

  7. spark streaming 流式计算---跨batch连接池共享(JVM共享连接池)

    在流式计算过程中,难免会连接第三方存储平台(redis,mysql...).在操作过程中,大部分情况是在foreachPartition/mapPartition算子中做连接操作.每一个分区只需要连接 ...

  8. springboot+Mybatis+MySql 一个update标签中执行多条update sql语句

    Mysql是不支持这种骚操作的,但是不代表并不能实现,只需要在jdbc配置文件中稍微做一下修改就行. driver=com.mysql.jdbc.Driver url=jdbc:mysql://127 ...

  9. 阶段3 1.Mybatis_07.Mybatis的连接池及事务_3 mybatis连接池的分类

    2.mybatis中的连接池     mybatis连接池提供了3种方式的配置:         配置的位置:             主配置文件SqlMapConfig.xml中的dataSourc ...

  10. mysql在命令行中,指定要连接的数据库?

    需求描述: mysql客户端,可以在登录到mysql数据库时,指定要连接到哪个数据库 这里进行一个测试. 测试过程: 1.mysql通过-D参数指定连接到test数据库 [mysql@redhat6 ...

随机推荐

  1. BeanUtils.copyProperties:曾经是我的女神,现在是我的毒药。

    前言 BeanUtils.copyProperties十有八九是你这些年工作中用的很多的其中一个,不管是Apache的还是Spring的. 网上的解释浩如烟海,我这边用一个超简单的例子直观展示给你看. ...

  2. Servlet 上

    Servlet 1.什么是Servlet Servlet即Server Applet是运行在Web服务器端的小程序 2.创建Servlet的三种方式 注意:从Tomcat10.0以后,我们统一用 ja ...

  3. java线程的interrup、tUninterruptibles.sleepUninterruptibly和sleep、wait

    参考: (1)https://blog.csdn.net/qq_36031640/article/details/116696685 (2)https://blog.csdn.net/liuxiao7 ...

  4. Windows上Dart安装

    过程 *1 去github上下载一个release包或者直接将flutter通过git clone下来 *2 将下载下来的flutter/bin添加到path中 *3 此时运行flutter或者flu ...

  5. 一文搞懂 OTP 双因素认证

    GitHub 在 2023 年 3 月推出了双因素认证(two-factor authentication)简称 2FA,并且承诺所有在 GitHub 上贡献的开发者在 2023 年底前启用双因素认证 ...

  6. Oracle:Ora-01652无法通过128(在temp表空间中)扩展temp段的过程-解决步骤

    现象:查询select * from v$sql时提示"Ora-01652无法通过128(在temp表空间中)扩展temp段的过程" 临时文件是不存储的,可以将数据库重启,重启后重 ...

  7. 两种方式,创建有返回值的DB2函数

    函数场景:路径信息由若干个机构编码组成,且一个机构编码是9位字符. 要求:获取路径信息,并且删除路径中包含'99'开头的机构编码. 从客户端及服务器端分别创建ignore99(pathinfo var ...

  8. 在deepin上使用Fleet开发SpringBoot 3.0.0项目

    前言 Fleet被称为是由 JetBrains 打造的下一代 IDE,目前出于公测状态,可以免费下载使用. SpringBoot 3.0.0最小支持是JDK 17,这或许是对于JDK8的断舍离迈出的重 ...

  9. Arduino Uno 引脚 –

    Arduino Uno 引脚 – Arduino Uno 板有 20 多个引脚,可用于许多不同的应用.在这篇文章中,我将为您提供 Arduino Uno 主要引脚的完整实用概述. 如果您刚开始使用 A ...

  10. 使用aop(肉夹馍)为BlazorServer实现统一异常处理

    背景 用户做一个操作往往对应一个方法的执行,而方法内部会调用别的方法,内部可能又会调用别的方法,从而形成一个调用链.我们一般是在最顶层的方法去加try,而不是调用链的每一层都去加try. 在web开发 ...