一次使用JDBC流式读取MYSQL数据库引发MYSQL服务性能问题的分析

背景:

        因公司业务需要,需要将mysql数据库中的一些数据放到redis中进行缓存,以提高查询效率。首先需要将存量数据初始化到redis中,现有存量数据约1000w,同事开发了个java小程序使用 stmt.setFetchSize(Integer.MIN_VALUE) 结合 ResultSet.TYPE_FORWARD_ONLYResultSet.CONCUR_READ_ONLY 模式的流式读取方式,将存量数据初始化到redis中。

问题:

        初始化过程中发现,程序运行一段时间(约4分钟)会和数据库连接断开,报如下错误,数据并未处理完毕。

Exception in thread "main" java.sql.SQLException: Error retrieving record: Unexpected Exception: java.io.EOFException message given: Can not read response from server. Expected to read 402 bytes, read 282 bytes before connection was unexpectedly lost.
Nested Stack Trace:
** BEGIN NESTED EXCEPTION **
java.io.EOFException
MESSAGE: Can not read response from server. Expected to read 402 bytes, read 282 bytes before connection was unexpectedly lost.
STACKTRACE:
java.io.EOFException: Can not read response from server. Expected to read 402 bytes, read 282 bytes before connection was unexpectedly lost.
        at com.mysql.cj.protocol.FullReadInputStream.readFully(FullReadInputStream.java:67)
        at com.mysql.cj.protocol.a.SimplePacketReader.readMessageLocal(SimplePacketReader.java:137)
        at com.mysql.cj.protocol.a.SimplePacketReader.readMessage(SimplePacketReader.java:102)
        at com.mysql.cj.protocol.a.SimplePacketReader.readMessage(SimplePacketReader.java:45)
        at com.mysql.cj.protocol.a.TimeTrackingPacketReader.readMessage(TimeTrackingPacketReader.java:62)
        at com.mysql.cj.protocol.a.TimeTrackingPacketReader.readMessage(TimeTrackingPacketReader.java:41)
        at com.mysql.cj.protocol.a.MultiPacketReader.readMessage(MultiPacketReader.java:66)
        at com.mysql.cj.protocol.a.MultiPacketReader.readMessage(MultiPacketReader.java:44)
        at com.mysql.cj.protocol.a.ResultsetRowReader.read(ResultsetRowReader.java:75)
        at com.mysql.cj.protocol.a.ResultsetRowReader.read(ResultsetRowReader.java:42)
        at com.mysql.cj.protocol.a.NativeProtocol.read(NativeProtocol.java:1587)
        at com.mysql.cj.protocol.a.result.ResultsetRowsStreaming.next(ResultsetRowsStreaming.java:194)
        at com.mysql.cj.protocol.a.result.ResultsetRowsStreaming.next(ResultsetRowsStreaming.java:62)
        at com.mysql.cj.jdbc.result.ResultSetImpl.next(ResultSetImpl.java:1813)
        at com.gtanzer.monitor.init.BigstateCache.MonitorCacheInit.set1(MonitorCacheInit.java:232)
        at com.gtanzer.monitor.init.BigstateCache.MonitorCacheInit.main(MonitorCacheInit.java:31)
** END NESTED EXCEPTION **
        at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:129)
        at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122)
        at com.mysql.cj.jdbc.result.ResultSetImpl.next(ResultSetImpl.java:1828)
        at com.gtanzer.monitor.init.BigstateCache.MonitorCacheInit.set1(MonitorCacheInit.java:232)
        at com.gtanzer.monitor.init.BigstateCache.MonitorCacheInit.main(MonitorCacheInit.java:31)
Caused by: java.io.EOFException: Can not read response from server. Expected to read 402 bytes, read 282 bytes before connection was unexpectedly lost.
        at com.mysql.cj.protocol.FullReadInputStream.readFully(FullReadInputStream.java:67)
        at com.mysql.cj.protocol.a.SimplePacketReader.readMessageLocal(SimplePacketReader.java:137)
        at com.mysql.cj.protocol.a.SimplePacketReader.readMessage(SimplePacketReader.java:102)
        at com.mysql.cj.protocol.a.SimplePacketReader.readMessage(SimplePacketReader.java:45)
        at com.mysql.cj.protocol.a.TimeTrackingPacketReader.readMessage(TimeTrackingPacketReader.java:62)
        at com.mysql.cj.protocol.a.TimeTrackingPacketReader.readMessage(TimeTrackingPacketReader.java:41)
        at com.mysql.cj.protocol.a.MultiPacketReader.readMessage(MultiPacketReader.java:66)
        at com.mysql.cj.protocol.a.MultiPacketReader.readMessage(MultiPacketReader.java:44)
        at com.mysql.cj.protocol.a.ResultsetRowReader.read(ResultsetRowReader.java:75)
        at com.mysql.cj.protocol.a.ResultsetRowReader.read(ResultsetRowReader.java:42)
        at com.mysql.cj.protocol.a.NativeProtocol.read(NativeProtocol.java:1587)
        at com.mysql.cj.protocol.a.result.ResultsetRowsStreaming.next(ResultsetRowsStreaming.java:194)
        at com.mysql.cj.protocol.a.result.ResultsetRowsStreaming.next(ResultsetRowsStreaming.java:62)
        at com.mysql.cj.jdbc.result.ResultSetImpl.next(ResultSetImpl.java:1813)
        ... 2 more

 问题分析及处理过程:       

 第一次尝试:

        根据报错,我们首先查看了mysql数据库的wait_timeoutinteractive_timeout两个参数,确认没有问题。

SHOW VARIABLES LIKE 'wait_timeout';
SHOW VARIABLES LIKE 'interactive_timeout';

        我们也视图通过调整在 JDBC URL 中 connectTimeoutsocketTimeout 的值来解决问题,但都无济于事,甚至因调整connectTimeoutsocketTimeout参数导致了数据库性能下降,影响到了我们的业务应用的正常运行,当时数据库的活跃连接数和等待线程数都超过了报警阈值发生了报警提醒。我们紧急停掉了初始化redis的java程序后恢复。

 第二次尝试:

        再次确认数据库使用的事务隔离级别是:READ COMMITTED,尝试使用/*!40001 SQL_NO_CACHE */,问题依然存在。

使用 SQL_NO_CACHE 有以下几方面的好处,具体取决于应用场景:

1. 防止查询结果缓存,确保实时性

  • 场景:在某些需要实时获取最新数据的查询中,不希望从缓存中读取旧的查询结果。
  • 好处:避免数据的延迟,确保查询结果是最新的。对于频繁变化的表,缓存的数据可能过时,SQL_NO_CACHE 保证每次查询都从硬盘或内存中获取最新数据。

2. 提高特定查询的性能

  • 场景:查询缓存可能对某些查询没有帮助,甚至会拖慢性能,特别是在大量写入操作的环境中,因为 MySQL 每次写操作都会使相关表的查询缓存失效。
  • 好处:对于频繁更新的表或较少被重复查询的表,禁用查询缓存可以减少缓存相关的开销,避免不必要的缓存维护操作,从而提高写入或更新效率。

3. 适用于复杂的或一次性查询

  • 场景:对于执行频率很低的复杂查询,缓存查询结果的意义不大,查询可能只运行一次,之后不会被再次调用。
  • 好处:避免将临时查询的结果存入缓存,节省内存资源,把缓存资源留给那些会被重复使用的查询结果。

4. 避免缓存污染

  • 场景:如果查询结果不太可能被再次使用,或查询数据的条件变化频繁,缓存结果可能导致缓存资源被浪费。
  • 好处:防止将这些无关紧要或频繁变化的查询结果加入缓存,保证查询缓存只用于那些可能被多次使用的查询,提升缓存的命中率。

5. 数据一致性

  • 场景:对于涉及敏感数据的查询,或者数据频繁变动时,确保每次查询获取的都是最新数据。
  • 好处:确保查询结果和数据库当前的状态完全一致,防止缓存带来的过时结果。

适用场景

  • 频繁写入的表:如果表中的数据变化非常频繁,禁用查询缓存可能会提高性能。
  • 实时数据查询:需要精确查询到最新的数据,不希望使用缓存。
  • 高并发写入场景:查询缓存频繁失效,写操作会导致缓存被清理,禁用查询缓存可以减少 MySQL 对缓存的维护开销。

何时避免使用

        如果查询本身是重复执行的、表数据更新频率较低,且查询缓存命中率较高的场景下,SQL_NO_CACHE 反而会降低查询性能,因为跳过了可以加速查询的缓存。

-- 查看当前会话的隔离级别
SELECT @@session.transaction_isolation;

-- 查看全局隔离级别
SELECT @@global.transaction_isolation;

   第三次尝试:

        咱们先说结果,最终我们分片查询的方式解决问题。

  •  首先分析在 JDBC 中,设置stmt.setFetchSize(Integer.MIN_VALUE) 和使用 ResultSet.TYPE_FORWARD_ONLYResultSet.CONCUR_READ_ONLY 进行流式读取的原理

        在 JDBC 中,设置 stmt.setFetchSize(Integer.MIN_VALUE) 和使用 ResultSet.TYPE_FORWARD_ONLYResultSet.CONCUR_READ_ONLY 进行流式读取时,背后的工作原理主要涉及数据从 MySQL 服务器传输到 JDBC 客户端的方式。了解这一过程可以帮助我们理解为何这种方法可能对 MySQL 服务端造成性能瓶颈。

底层原理

  1. 默认的结果集加载机制:

    • 在默认情况下,JDBC 会将查询结果集一次性从 MySQL 服务器加载到客户端内存中。无论查询结果的大小如何,整个结果集都会通过网络从 MySQL 服务器传输到客户端,并加载到内存中。这种方法适合小型结果集,但对于大型结果集会导致内存溢出。
  2. 流式读取机制:

    • 设置 stmt.setFetchSize(Integer.MIN_VALUE) 配置 JDBC 以流式读取的方式获取数据。流式读取意味着数据不会一次性全部加载,而是逐条或逐批次从服务器拉取。
    • MySQL 服务器在执行查询时,会逐步将数据发送给 JDBC 客户端,而不是一次性将所有数据发送到客户端内存。这种读取方式依赖于 TYPE_FORWARD_ONLYCONCUR_READ_ONLY 的组合,这表明结果集只能顺序前进,不允许回退或修改。
  3. 流式读取的实现:

    • JDBC 驱动程序通过在服务端和客户端之间保持一个长时间的、持续的网络连接,将查询结果按需发送给客户端。客户端在需要下一行数据时,向 MySQL 服务器发送请求,而服务器将相应的行返回给客户端。
    • 结果集并不被缓存,客户端每次只请求一行或一小部分数据。因此,这种机制适合处理超大数据集,因为它避免了客户端内存的过度消耗。

对 MySQL 服务端的影响

尽管流式读取在客户端有效地解决了内存溢出问题,但它可能在 MySQL 服务端引发性能瓶颈。主要原因如下:

  1. 长时间持有数据库资源:

    • 当使用流式读取时,MySQL 必须在整个查询过程中保持数据库连接打开,直到客户端处理完所有的数据。这会导致数据库连接长时间占用,而这些连接会占用 MySQL 的资源(如内存、CPU、I/O 等)。
    • 如果有大量客户端同时进行流式查询,MySQL 服务器的资源可能会被耗尽,特别是在并发流式查询较多时,导致系统响应变慢或甚至连接耗尽。
  2. 锁机制可能导致性能瓶颈:

    • 在一些情况下,MySQL 可能需要锁住查询的数据集或表,直到流式读取完成。这尤其在高隔离级别(如 REPEATABLE READ)时可能出现。锁的持续时间较长,可能会影响其他读写操作。
    • 锁竞争:如果有其他事务在等待这些资源(如表锁或行锁),流式查询时间越长,锁的竞争就越激烈,从而影响系统的整体性能。
  3. 缺乏缓存利用:

    • 流式读取过程中,MySQL 服务器无法将整个结果集缓存,因为查询的数据是逐步发送的。因此,MySQL 每次都需要从存储引擎读取数据,而不是从缓存中读取,导致 I/O 操作增加,尤其是在处理大型数据集时,频繁的 I/O 操作会显著增加 MySQL 的负载。
  4. 网络开销:

    • 流式读取意味着每一行数据的传输都需要进行网络交互。对于大数据量查询,这种频繁的网络交互会导致网络负载增加,尤其在网络延迟较高的情况下,性能瓶颈更加明显。
    • 如果客户端与服务器之间的网络带宽或延迟存在问题,数据传输速度会变慢,进一步降低 MySQL 的响应性能。
  5. I/O 压力增加:

    • MySQL 必须为每个流式查询执行频繁的磁盘 I/O 操作,尤其是当查询没有使用索引时,可能会导致全表扫描。这种操作会增加 MySQL 的 I/O 压力,影响整体性能。
    • 如果服务器硬盘速度较慢或者有其他并发 I/O 操作,流式读取的性能会大大降低。

性能瓶颈示例

  • 场景 1:长时间持有连接

    • 假设你查询一个包含数百万行数据的表,使用 setFetchSize(Integer.MIN_VALUE)。由于每次只获取一行数据,整个查询可能需要几个小时才能完成。在这段时间内,MySQL 需要一直维持这个连接,消耗资源。如果有多个客户端执行类似查询,MySQL 可能无法处理更多的并发查询。
  • 场景 2:频繁网络请求导致延迟

    • 如果网络带宽较低或延迟较高,客户端和服务器之间的频繁通信将显著降低查询性能。特别是当数据量巨大时,这种影响会更为明显,可能导致查询时间远远超出预期。

总结

使用 stmt.setFetchSize(Integer.MIN_VALUE) 进行流式读取虽然解决了客户端内存溢出的问题,但会给 MySQL 服务器带来一些性能瓶颈,包括长时间占用连接资源、锁竞争、I/O 压力增大以及频繁的网络交互。这种方式适合处理大数据集,但在高并发环境或网络状况较差时,可能会导致性能问题。

要缓解这些瓶颈,可以考虑:

  • 优化查询,确保使用了合适的索引。
  • 使用适当的 fetchSize,而不是使用最小值。
  • 监控和调整 MySQL 的连接池大小,确保不会因为长时间查询占用连接池而导致系统过载。

       

  • 进一步分析流式读取可能带来的mysql服务端性能瓶颈

        使用 stmt.setFetchSize(Integer.MIN_VALUE) 结合 ResultSet.TYPE_FORWARD_ONLYResultSet.CONCUR_READ_ONLY 模式的流式读取方式,虽然可以有效处理大数据集而不会占用大量内存,但它可能带来一些性能瓶颈。以下是一些常见的瓶颈及其可能的影响:

1. 网络延迟与带宽

  • 瓶颈:每次从数据库流式获取一条数据时,都会进行网络交互。如果网络延迟较高,逐行读取的频繁网络通信会导致性能下降,尤其是当你查询大量数据时。
  • 影响:网络传输速度变成了数据获取的主要限制因素,查询结果的返回速度将受到网络质量的直接影响。

2. 数据库的 I/O 开销

  • 瓶颈:由于数据是逐行从数据库读取的,如果查询涉及大量数据,数据库的 I/O 操作将非常频繁。这可能导致较高的 I/O 开销,尤其是如果数据库存储在慢速磁盘上或并发查询很多时。
  • 影响:I/O 瓶颈可能会导致查询响应时间变长,特别是在读取大表时。

3. JDBC 驱动的效率

  • 瓶颈:不同的 JDBC 驱动程序对流式读取的实现可能有不同的性能表现。有些驱动在处理 setFetchSize(Integer.MIN_VALUE) 时,可能并没有对流式读取做出最优化的处理,导致性能欠佳。
  • 影响:某些 JDBC 驱动程序可能在高并发或大数据量时表现不佳,因此选择一个合适的 JDBC 驱动非常重要。

4. 无法使用缓存(SQL_NO_CACHE)

  • 瓶颈:在这种流式读取模式下,数据库无法缓存整个结果集。每次请求数据时都需要从存储层读取,而不是从缓存中快速返回数据。
  • 影响:对于相同查询的多次请求,响应时间可能会明显变慢,因为缓存未被利用,数据库必须重新读取所有数据。

5. 数据库连接的长时间占用

  • 瓶颈:当你使用流式读取时,数据库连接在整个查询期间会一直保持打开状态。如果你的查询需要很长时间才能完成(例如处理非常大的数据集),该连接会被长时间占用,导致其他并发连接资源不足。
  • 影响:在高并发系统中,长时间的连接占用可能会减少系统的吞吐量,限制并发查询的能力。

6. 事务控制与锁机制

  • 瓶颈:在某些数据库中,流式读取可能在整个读取过程中保持锁定,特别是在使用默认的事务隔离级别(如 REPEATABLE READSERIALIZABLE)时。这会导致其他事务无法更新该数据表,进而引发锁等待和死锁问题。
  • 影响:长时间运行的查询可能会导致数据库中锁的竞争,影响到其他读写操作,降低整体系统的并发性能。

7. 数据库端的资源消耗

  • 瓶颈:如果数据库服务器正在处理多个流式读取查询,每个查询都需要占用一定的资源,比如内存、I/O、连接等。如果同时有大量流式读取请求,可能会导致数据库资源耗尽。
  • 影响:数据库服务器可能会因为资源不足而出现性能下降,响应时间增加,甚至连接中断。

8. 数据一致性问题

  • 瓶颈:流式读取时,数据在客户端逐行读取的过程中,数据库中的数据可能会发生变化,尤其是在长时间读取操作中。这个过程中如果有其他事务在对表进行更新,可能会导致读取到的数据不一致。
  • 影响:除非使用较高的事务隔离级别(如 SERIALIZABLE),否则在并发环境中读取的大量数据可能会导致数据不一致问题。

优化建议:

  1. 增加批量读取的 Fetch Size:如果你发现性能瓶颈,可以尝试在适当的场景下增加 fetchSize 值(例如 1000 或更高),这样每次读取的行数会更多,减少与数据库之间的网络交互频率。

    stmt.setFetchSize(1000); // 每次读取1000行
    
  2. 优化数据库索引:确保查询走索引,避免不必要的全表扫描。对于大表来说,未优化的查询可能导致极高的 I/O 开销。

  3. 使用连接池:如果你频繁使用长时间的数据库连接,确保数据库连接使用了连接池技术,如 HikariCP 等。这样可以更好地管理和重用连接,避免长时间占用带来的资源浪费。

  4. 优化网络延迟:确保客户端与数据库之间的网络连接足够快速和稳定,减少网络传输带来的延迟。

  5. 分片查询:对于非常大的数据集,可以考虑将查询数据分片处理,避免单次查询返回太多数据,导致性能下降。

  6. 监控并发情况:使用数据库的监控工具查看系统负载情况,确保流式读取不会导致数据库资源耗尽。

        通过合理配置 fetchSize 和优化查询,流式读取可以大大减少内存消耗,但要注意网络、I/O 和数据库连接的管理,避免性能瓶颈。

        

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值