简体   繁体   English

调查 JDBC 和 MySQL 中的慢速简单查询

[英]Investigating slow simple queries in JDBC and MySQL

PreparedStatement.executeQuery() is taking ~20x longer to execute than if it were run directly via the shell. PreparedStatement.executeQuery()的执行时间比直接通过 shell 运行的时间长约 20 倍。 I've logged with timers to determine that this method is the culprit.我已经用计时器记录以确定这种方法是罪魁祸首。

The query and some DB info ( ignoring the Java issue for the moment ):查询和一些数据库信息(暂时忽略 Java 问题):

mysql> SELECT username from users where user_id = 1; // lightning fast // 闪电般的快

Running that same query 1,000 times via mysqlslap is also lightning fast.通过 mysqlslap 运行相同的查询 1000 次也非常快。

mysqlslap --create-schema=mydb --user=root -p --query="select username from phpbb_users where user_id = 1" --number-of-queries=1000 --concurrency=1

Benchmark
        Average number of seconds to run all queries: 0.051 seconds
        Minimum number of seconds to run all queries: 0.051 seconds
        Maximum number of seconds to run all queries: 0.051 seconds
        Number of clients running queries: 1
        Average number of queries per client: 1000

The Problem: Performing the same query in JDBC slows things significantly.问题:在 JDBC 中执行相同的查询会显着减慢速度。 In a for loop calling the below queryUsername() 1,000 times (this is called in the Main method, which isn't shown here) takes around 872ms.在 for 循环中调用以下queryUsername() 1,000 次(在 Main 方法中调用,此处未显示)大约需要 872 毫秒。 That's ~17x slower.这慢了约 17 倍。 I've tracked down the heavy usage by placing timers in various spots (omitted some for brevity).我通过在不同的位置放置计时器来追踪大量使用(为了简洁省略了一些)。 The primary suspect is stmt.executeQuery() which took 776ms of the 872ms runtime.主要嫌疑人是stmt.executeQuery() ,它占用了 872 毫秒运行时间中的 776 毫秒。

public static String queryUsername() {
        String username = "";
        // DBCore.getConnection() returns HikariDataSource.getConnection() implementation exactly as per https://www.baeldung.com/hikaricp
        try (Connection connection = DBCore.getConnection(); 
            PreparedStatement stmt = connection.prepareStatement("SELECT username from phpbb_users where user_id = ?");) {
            stmt.setInt(1, 1);  // just looking for user_id 1 for now
            // Google timer used to measure how long executeQuery() is taking
            // Another Timer is used outside of this method call to see how long
            // total execution takes. 
            // Approximately 1 second in for loop calling this method 1000 times
            Stopwatch s = Stopwatch.createStarted(); 
            try (ResultSet rs = stmt.executeQuery();) {
                s.stop(); // stopping the timer after executeQuery() has been called
                timeElapsed  += s.elapsed(TimeUnit.MICROSECONDS);
                while (rs.next())
                {
                    username = rs.getString("username"); // the query returns 1 record
                }
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        
        return username;
    }

Additional context and things tried :其他上下文和尝试的事情

解释声明

在此处输入图像描述

  • SHOW OPEN TABLES has several tables open, but all have In_use=0 and Name_locked=0. SHOW OPEN TABLES有几个打开的表,但都具有 In_use=0 和 Name_locked=0。
  • SHOW FULL PROCESSLIST looks healthy. SHOW FULL PROCESSLIST看起来很健康。
  • user_id is an indexed primary key user_id 是索引主键
  • The Server is an Upcloud $5/month 1-Core, 1GB RAM running Ubuntu 20.04.1 LTS (GNU/Linux 5.4.0-66-generic x86_64).服务器是 Upcloud 每月 5 美元的 1 核 1GB RAM,运行 Ubuntu 20.04.1 LTS(GNU/Linux 5.4.0-66-generic x86_64)。 Mysql Ver 8.0.23-0ubuntu0.20.04.1 for Linux on x86_64 ((Ubuntu)) Mysql 版本 8.0.23-0ubuntu0.20.04.1 用于 x86_64 上的 Linux((Ubuntu))
  • JDBC Driver is mysql-connector-java_8.0.23.jar, which was obtained from mysql-connector-java_8.0.23-1ubuntu20.04_all via https://dev.mysql.com/downloads/connector/j/ JDBC Driver is mysql-connector-java_8.0.23.jar, which was obtained from mysql-connector-java_8.0.23-1ubuntu20.04_all via https://dev.mysql.com/downloads/connector/j/

服务器 HTOP

Don't reconnect each time.不要每次都重新连接。 Open the connection at the start;一开始就打开连接; reuse it until the web page (or program) is finished.重复使用它,直到 web 页面(或程序)完成。

Chances are that you are comparing different realities.您可能正在比较不同的现实。

When running mysqlslap you are most likely using Unix Domain Sockets in the communication between the tool and MySQL server.运行 mysqlslap 时,您很可能在工具和 MySQL 服务器之间的通信中使用 Unix 域 Sockets。 Try changing that to TCP and you should observe an immediate performance drop.尝试将其更改为 TCP,您应该会立即观察到性能下降。 Connector/J, on the other hand, creates TCP based connections by default (Unix Domain Sockets can be used but only by using a third party library).另一方面,Connector/J 默认创建基于 TCP 的连接(可以使用 Unix 域 Sockets,但只能使用第三方库)。

Also, in mysqlslap you are running a simple query directly, which are handled by a COM_QUERY protocol command.此外,在 mysqlslap 中,您直接运行一个简单的查询,由 COM_QUERY 协议命令处理。 In the Java sample you are preparing the query first and then executing it.在 Java 示例中,您首先准备查询,然后执行它。 Depending on how Connector/J is configured this may result in a single COM_QUERY protocol command or a pair of commands, namely, COM_STMT_PREPARE and COM_STMT_EXECUTE.根据连接器/J 的配置方式,这可能会导致单个 COM_QUERY 协议命令或一对命令,即 COM_STMT_PREPARE 和 COM_STMT_EXECUTE。 Connector/J is also affected by how its statement caches are configured (and/or the CP ones). Connector/J 还受其语句缓存的配置方式(和/或 CP 缓存)的影响。 However, you are only measuring the executeQuery part so, theoretically, Connector/J could be being favored here.但是,您只测量executeQuery部分,因此理论上,Connector/J 可能在这里受到青睐。

Finally, unless you actually come up with a use case where you guarantee that both executions are effectively doing the same work under the same circumstances, you can compare results and point out differences, but you can't take any conclusions from it.最后,除非你真的想出一个用例,保证两个执行在相同情况下有效地完成相同的工作,否则你可以比较结果并指出差异,但你不能从中得出任何结论。 For example, it's not that hard to introduce caches and make those simple iterations even completely skip communicating to the server... that would make things extremely fast.例如,引入缓存并让那些简单的迭代甚至完全跳过与服务器的通信并不难……这将使事情变得非常快。

move borrowing connection and Stopwatch related code out of method.将借用连接和秒表相关代码移出方法。 then measure as:然后测量为:

 Stopwatch s = Stopwatch.createStarted();
 try (Connection con = ....)  {
     for (int i=0; i < 1000; i++)  {
            queryUsername(   con   );
      }
 }
 s.stop();
 print s.elapsed(TimeUnit.MICROSECONDS);

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM