【问题标题】:Investigating slow simple queries in JDBC and MySQL调查 JDBC 和 MySQL 中的慢速简单查询
【发布时间】:2021-03-05 02:03:50
【问题描述】:

PreparedStatement.executeQuery() 的执行时间比直接通过 shell 运行的时间长约 20 倍。我已经用计时器记录以确定这种方法是罪魁祸首。

查询和一些数据库信息(暂时忽略 Java 问题):

mysql> SELECT username from users where user_id = 1; // 闪电般的速度

通过 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

问题:在 JDBC 中执行相同的查询会显着降低速度。在 for 循环中调用下面的queryUsername() 1,000 次(这在 Main 方法中调用,此处未显示)大约需要 872 毫秒。慢了约 17 倍!我通过在不同的位置放置计时器来追踪大量使用(为了简洁省略了一些)。主要嫌疑人是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;
    }

其他上下文和尝试的事情

  • SHOW OPEN TABLES 有几张表打开,但都有 In_use=0 和 Name_locked=0。
  • SHOW FULL PROCESSLIST 看起来很健康。
  • user_id 是索引主键
  • 服务器是 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))
  • JDBC Driver为mysql-connector-java_8.0.23.jar,通过https://dev.mysql.com/downloads/connector/j/从mysql-connector-java_8.0.23-1ubuntu20.04_all获取

【问题讨论】:

  • 更新:几个月后...我能够通过将 JDBC 驱动程序更新为 mysql-connector-java-8.0.26.jar 将查询时间减少一半
  • 更新:通过切换到 MariaDB Connector/J 2.7.4 kb.askmonty.org/en/about-the-mariadb-java-client,我能够实现更快的查询时间

标签: java mysql performance jdbc hikaricp


【解决方案1】:

不要每次都重新连接。一开始就打开连接;在网页(或程序)完成之前重复使用它。

【讨论】:

  • 或者也许将 DBCP 用于严肃的程序。
  • 我的想法很复杂。 Connection 对象是通过 HikariDataSource.getConnection() 获得的。这是不正确的吗? (这就是上面的 DBCore.getConnection() 所做的)
  • 我希望 DBCP 需要 一些 时间。将其留给连接到相同或不同网页的多个用户。您的测试 Select 尽可能简单。 (SELECT 1 更简单。)您似乎在做的是定时连接,而不是SELECT。我是否误读了代码?
  • 循环在哪里? Slap 正在运行整个事情 1000 次?
  • OP 正在使用 - 我假设名称是 - 连接池 (HikariCP),所以基本上他们已经在重用连接。
【解决方案2】:

您可能在比较不同的现实。

在运行 mysqlslap 时,您很可能在工具和 MySQL 服务器之间的通信中使用 Unix 域套接字。尝试将其更改为 TCP,您应该会立即观察到性能下降。另一方面,Connector/J 默认创建基于 TCP 的连接(可以使用 Unix 域套接字,但只能使用第三方库)。

另外,在 mysqlslap 中,您直接运行一个简单的查询,由 COM_QUERY 协议命令处理。在 Java 示例中,您首先准备查询,然后执行它。根据连接器/J 的配置方式,这可能会导致单个 COM_QUERY 协议命令或一对命令,即 COM_STMT_PREPARE 和 COM_STMT_EXECUTE。 Connector/J 还受其语句缓存的配置方式(和/或 CP 缓存)的影响。但是,您只测量了executeQuery 部分,因此理论上,Connector/J 可能会受到青睐。

最后,除非你真的想出一个用例,保证两个执行在相同的情况下有效地完成相同的工作,你可以比较结果并指出差异,但你不能从中得出任何结论.例如,引入缓存并让那些简单的迭代甚至完全跳过与服务器的通信并不难……这将使事情变得非常快。

【讨论】:

  • 非常感谢您的想法。我已经着手实现了 Unix Domain Sockets 库junixsocket 来比较性能。计算时间与 Hikari (TCP) 相同。 Mysqlslap 的运行速度仍然快得惊人。我有点难过。我们在 JDBC 中实现了每个查询约 1 毫秒,而 slap 中每个查询约 0.3 毫秒。我还继续在 slap 查询中添加了 --protocol=tcp 标志,并且时间没有改变。我开始认为,将 1 毫秒的 JDBC 时间减少到我们在耳光中看到的那个甜蜜的 ~.3 毫秒是不可能的,但也许你有想法。你怎么看?
  • 我不能代表 junixsocket,但与 mysql 命令行工具相比,我预计 Connector/J 的速度会有所下降。后者在 C 中实现并使用libmysqlclient,因此它们更接近“裸机”。
  • 你说添加 --protocol=tcp 并没有改变结果,但这不是我所看到的:Average number of seconds to run all queries: 1.004 seconds vs Average number of seconds to run all queries: 0.137 seconds 在我的机器中
【解决方案3】:

将借用连接和秒表相关代码移出方法。然后测量为:

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

【讨论】:

  • 这个已经测量过了。耗时 872 毫秒。大部分时间来自 queryUsername() 方法。根据 872ms 运行时的 OP 776ms 来自 queryUsername()。使用单个连接与使用通过 HikariDataSource 池获得的连接相比没有任何改进。
  • 检查 DBCore.getConnection() 为连接设置的任何选项。例如事务隔离级别、自动提交、只读、模式、各种缓存选项...其中一些的默认设置可能不同对于 jdbc 和 mysqlslap
  • 在运行期间监控 GC 活动。也可以使用github.com/openjdk/jmh 进行测量
  • 可能是 --iterations 而不是 --number-of-queries 用于 mysqlslap?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-17
  • 2021-12-15
  • 2016-01-10
  • 2023-04-09
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多