在回答这个问题之前,首先我们看看 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. ipmitool配置机器的BMC

    一.设置IP地址 1.确定操作对象 #ipmitool mc info 输出中"Device Revision"是命令的操作对象 2.设置BMC IP # ipmitool -I ...

  2. 创建第一个C语言文件

    创建第一个C语言文件 新建=>项目=>空项目 创建.c文件 我们学的是C语言,c++就不写了 调整字体 快捷键:Ctlr + 鼠标滚轮 通过工具调整 工具库与main()函数 打开一个工具 ...

  3. redis基本数据类型 List

    127.0.0.1:6379> LPUSH test a (integer) 1 127.0.0.1:6379> LPUSH test b (integer) 2 127.0.0.1:63 ...

  4. 创建Oracle数据库实例、配置PLSQL连接数据库实例

    方法一:仅安装Oracle即时客户端: 1.安装Oracle即时客户端Instant Client Setup.exe,配置环境变量 2.安装PLSQL工具,配置Oracle主目录名和OCI库 3.配 ...

  5. 时髦称呼:SQL递归"语法糖"的用法

    Oracle函数sys_connect_by_path 详解 语法:Oracle函数:sys_connect_by_path 主要用于树查询(层次查询) 以及 多列转行.其语法一般为:       s ...

  6. keepalived部署+nginx高可用

    nginx+keepalived搞性能web网络架构实战配置: 环境准备: keepalived+nginx-1: 192.168.1.23 keepalived+nginx-2: 192.168.1 ...

  7. P9754 [CSP-S 2023] 结构体 题解

    大模拟的话,大家应该都会,主要就是容易写挂. 操作 1 先理解什么叫做对齐规则.这点我们以样例 2 进行解释: struct a { int aa; short ab; long ac; byte a ...

  8. mac os 升级到13后,系统免密失败

    # sudo vim /etc/ssh/ssh_config # 添加以下内容 PubkeyAcceptedKeyTypes +ssh-rsa

  9. 词!自然语言处理之词全解和Python实战!

    本文全面探讨了词在自然语言处理(NLP)中的多维角色.从词的基础概念.形态和词性,到词语处理技术如规范化.切分和词性还原,文章深入解析了每一个环节的技术细节和应用背景.特别关注了词在多语言环境和具体N ...

  10. GCD Inside: GCD 数据结构

    1 OS_object OS_object由下面宏OS_OBJECT_DEC_BASE扩展而来: // 1. os/object.h OS_OBJECT_DECL_BASE(object, NSObj ...