因公司业务需要,需要将mysql数据库中的一些数据放到redis中进行缓存,以提高查询效率。首先需要将存量数据初始化到redis中,现有存量数据约1000w,同事开发了个java小程序使用 stmt.setFetchSize(Integer.MIN_VALUE)
结合 ResultSet.TYPE_FORWARD_ONLY
和 ResultSet.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_timeout
或 interactive_timeout两个参数,确认没有问题。
SHOW VARIABLES LIKE 'wait_timeout';
SHOW VARIABLES LIKE 'interactive_timeout';
我们也视图通过调整在 JDBC URL 中 connectTimeout
和 socketTimeout
的值来解决问题,但都无济于事,甚至因调整connectTimeout
和 socketTimeout参数导致了数据库性能下降,影响到了我们的业务应用的正常运行,当时数据库的活跃连接数和等待线程数都超过了报警阈值发生了报警提醒。我们紧急停掉了初始化redis的java程序后恢复。
再次确认数据库使用的事务隔离级别是:READ COMMITTED,尝试使用/*!40001 SQL_NO_CACHE */,问题依然存在。
使用 SQL_NO_CACHE
有以下几方面的好处,具体取决于应用场景:
SQL_NO_CACHE
保证每次查询都从硬盘或内存中获取最新数据。 如果查询本身是重复执行的、表数据更新频率较低,且查询缓存命中率较高的场景下,SQL_NO_CACHE
反而会降低查询性能,因为跳过了可以加速查询的缓存。
-- 查看当前会话的隔离级别
SELECT @@session.transaction_isolation;
-- 查看全局隔离级别
SELECT @@global.transaction_isolation;
咱们先说结果,最终我们分片查询的方式解决问题。
stmt.setFetchSize(Integer.MIN_VALUE)
和使用 ResultSet.TYPE_FORWARD_ONLY
、ResultSet.CONCUR_READ_ONLY
进行流式读取的原理 在 JDBC 中,设置 stmt.setFetchSize(Integer.MIN_VALUE)
和使用 ResultSet.TYPE_FORWARD_ONLY
、ResultSet.CONCUR_READ_ONLY
进行流式读取时,背后的工作原理主要涉及数据从 MySQL 服务器传输到 JDBC 客户端的方式。了解这一过程可以帮助我们理解为何这种方法可能对 MySQL 服务端造成性能瓶颈。
默认的结果集加载机制:
流式读取机制:
stmt.setFetchSize(Integer.MIN_VALUE)
配置 JDBC 以流式读取的方式获取数据。流式读取意味着数据不会一次性全部加载,而是逐条或逐批次从服务器拉取。TYPE_FORWARD_ONLY
和 CONCUR_READ_ONLY
的组合,这表明结果集只能顺序前进,不允许回退或修改。流式读取的实现:
尽管流式读取在客户端有效地解决了内存溢出问题,但它可能在 MySQL 服务端引发性能瓶颈。主要原因如下:
长时间持有数据库资源:
锁机制可能导致性能瓶颈:
REPEATABLE READ
)时可能出现。锁的持续时间较长,可能会影响其他读写操作。缺乏缓存利用:
网络开销:
I/O 压力增加:
场景 1:长时间持有连接
setFetchSize(Integer.MIN_VALUE)
。由于每次只获取一行数据,整个查询可能需要几个小时才能完成。在这段时间内,MySQL 需要一直维持这个连接,消耗资源。如果有多个客户端执行类似查询,MySQL 可能无法处理更多的并发查询。场景 2:频繁网络请求导致延迟
使用 stmt.setFetchSize(Integer.MIN_VALUE)
进行流式读取虽然解决了客户端内存溢出的问题,但会给 MySQL 服务器带来一些性能瓶颈,包括长时间占用连接资源、锁竞争、I/O 压力增大以及频繁的网络交互。这种方式适合处理大数据集,但在高并发环境或网络状况较差时,可能会导致性能问题。
要缓解这些瓶颈,可以考虑:
fetchSize
,而不是使用最小值。 使用 stmt.setFetchSize(Integer.MIN_VALUE)
结合 ResultSet.TYPE_FORWARD_ONLY
和 ResultSet.CONCUR_READ_ONLY
模式的流式读取方式,虽然可以有效处理大数据集而不会占用大量内存,但它可能带来一些性能瓶颈。以下是一些常见的瓶颈及其可能的影响:
setFetchSize(Integer.MIN_VALUE)
时,可能并没有对流式读取做出最优化的处理,导致性能欠佳。REPEATABLE READ
或 SERIALIZABLE
)时。这会导致其他事务无法更新该数据表,进而引发锁等待和死锁问题。SERIALIZABLE
),否则在并发环境中读取的大量数据可能会导致数据不一致问题。增加批量读取的 Fetch Size:如果你发现性能瓶颈,可以尝试在适当的场景下增加 fetchSize
值(例如 1000 或更高),这样每次读取的行数会更多,减少与数据库之间的网络交互频率。
stmt.setFetchSize(1000); // 每次读取1000行
优化数据库索引:确保查询走索引,避免不必要的全表扫描。对于大表来说,未优化的查询可能导致极高的 I/O 开销。
使用连接池:如果你频繁使用长时间的数据库连接,确保数据库连接使用了连接池技术,如 HikariCP 等。这样可以更好地管理和重用连接,避免长时间占用带来的资源浪费。
优化网络延迟:确保客户端与数据库之间的网络连接足够快速和稳定,减少网络传输带来的延迟。
分片查询:对于非常大的数据集,可以考虑将查询数据分片处理,避免单次查询返回太多数据,导致性能下降。
监控并发情况:使用数据库的监控工具查看系统负载情况,确保流式读取不会导致数据库资源耗尽。
通过合理配置 fetchSize
和优化查询,流式读取可以大大减少内存消耗,但要注意网络、I/O 和数据库连接的管理,避免性能瓶颈。