踩坑记:MySQL 连接 URL 缺失useCursorFetch参数引发的 Java 内存溢出惨案

在 Java 后端开发中,数据库交互是核心环节,而 MySQL 作为主流关系型数据库,其 JDBC 连接参数的配置稍有不慎,就可能引发致命的内存问题。近期我们线上服务就因 MySQL 连接 URL 中缺失useCursorFetch=true&defaultFetchSize=10000这两个关键参数,导致频繁触发 OOM(内存溢出),服务频繁宕机。本文将复盘整个问题过程,拆解参数作用,给出解决方案,并总结避坑要点。

一、问题爆发:无征兆的 OOM 与服务崩溃

1. 现象描述

线上某数据同步服务运行近半年后,突然出现偶发的 Full GC,随后逐渐演变为频繁 OOM,服务进程直接被系统杀死。查看监控面板发现:

  • 老年代内存占用持续飙升,GC 后无明显回落;
  • 线程 Dump 显示大量om.mysql.jdbc.MysqlIO.readSingleRowSet相关线程;

"main" #1 prio=5 os_prio=0 tid=0x0000020ee5845800 nid=0x4234 runnable 0x0000006dc86fe000

java.lang.Thread.State: RUNNABLE

at com.mysql.jdbc.MysqlIO.unpackBinaryResultSetRow(MysqlIO.java:4662)

at com.mysql.jdbc.MysqlIO.nextRow(MysqlIO.java:1984)

at com.mysql.jdbc.MysqlIO.readSingleRowSet(MysqlIO.java:3423)

at com.mysql.jdbc.MysqlIO.getResultSet(MysqlIO.java:481)

at com.mysql.jdbc.MysqlIO.readResultsForQueryOrUpdate(MysqlIO.java:3118)

at com.mysql.jdbc.MysqlIO.readAllResults(MysqlIO.java:2288)

at com.mysql.jdbc.ServerPreparedStatement.serverExecute(ServerPreparedStatement.java:1462)

  • locked <0x00000006c9e86478> (a com.mysql.jdbc.JDBC4Connection)

  • locked <0x00000006c9eb4b00> (a com.mysql.jdbc.JDBC4ServerPreparedStatement)

at com.mysql.jdbc.ServerPreparedStatement.executeInternal(ServerPreparedStatement.java:847)

  • locked <0x00000006c9eb4b00> (a com.mysql.jdbc.JDBC4ServerPreparedStatement)

at com.mysql.jdbc.PreparedStatement.executeQuery(PreparedStatement.java:2310)

  • locked <0x00000006c9e86478> (a com.mysql.jdbc.JDBC4Connection)

  • locked <0x00000006c9eb4b00> (a com.mysql.jdbc.JDBC4ServerPreparedStatement)

at com.alibaba.druid.filter.FilterChainImpl.preparedStatement_executeQuery(FilterChainImpl.java:2714)

at com.alibaba.druid.filter.FilterEventAdapter.preparedStatement_executeQuery(FilterEventAdapter.java:465)

at com.alibaba.druid.filter.FilterChainImpl.preparedStatement_executeQuery(FilterChainImpl.java:2711)

at com.alibaba.druid.proxy.jdbc.PreparedStatementProxyImpl.executeQuery(PreparedStatementProxyImpl.java:132)

at com.alibaba.druid.pool.DruidPooledPreparedStatement.executeQuery(DruidPooledPreparedStatement.java:227)

at com.doeis.utils.TestClass.main(TestClass.java:19)

2. 初步排查

排查代码逻辑:数据同步服务核心逻辑是从 MySQL 读取百万级数据,进行清洗后写入另一存储。SQL 语句为简单的SELECT * FROM big_table ,未使用分页,而是通过ResultSet遍历读取。

排查服务器资源:内存配置为 8G,JVM 堆内存分配 4G,此前运行稳定,近期仅数据量从百万级增长至千万级,未做代码和配置变更。

java 复制代码
PreparedStatement insertStmt = null;
		ResultSet rs = null;
		try {
			StringBuffer sql = new StringBuffer("SELECT * from big_table ");
			Connection conn = DBManager.getConnection();
			insertStmt = conn.prepareStatement(sql.toString());
	        rs = insertStmt.executeQuery();
	        while (rs.next()){
	        	 rs.getInt("id");
	        }
		}catch (Exception e) {
			
		}

二、根因定位:MySQL JDBC 的默认数据读取机制

1. 为什么会 OOM?

MySQL JDBC 驱动(mysql-connector-java)默认的结果集读取方式是 "一次性加载所有数据到内存":当执行查询语句后,驱动会将符合条件的所有结果集数据一次性从数据库服务器拉取到客户端(Java 应用)的内存中 ,再通过ResultSet遍历。

当查询结果集达到千万级时,即使单条数据仅 1KB,千万级数据也会占用近 10GB 内存,远超 JVM 堆内存上限,直接触发 OOM。

2. 关键参数的作用

我们缺失的useCursorFetch=truedefaultFetchSize=10000,正是用来解决这个问题的核心参数:

  • useCursorFetch=true:启用游标获取(Cursor Fetch)模式。开启后,JDBC 驱动不再一次性拉取所有数据,而是通过数据库游标分批读取,仅在客户端内存中保留当前批次的数据;
  • defaultFetchSize=10000:设置默认的批次读取大小,即每次从数据库拉取 10000 条数据到客户端内存,遍历完这批数据后,再拉取下一批,直至遍历完所有结果。

简单来说,这两个参数的组合,将 "一次性加载全部数据" 改为**"分批加载、按需读取**",从根本上限制了结果集占用的内存大小。

三、解决方案:配置参数 + 代码优化

1. 第一步:修改 MySQL 连接 URL

在原有连接 URL 基础上,添加游标和批次参数:

// 优化前

jdbc.druid.url=jdbc:mysql://127.0.0.1:3306/dbname?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true

// 优化后

jdbc.druid.url=jdbc:mysql://127.0.0.1:3306/dbname?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useCursorFetch=true&defaultFetchSize=10000

参数说明

  • useCursorFetch=true:必须开启,否则defaultFetchSize不生效;
  • defaultFetchSize:建议根据业务调整(如 1000、5000、10000),过大仍可能占用较多内存,过小则会增加数据库交互次数,影响性能。

四、避坑总结:MySQL JDBC 连接的关键注意事项

  1. 大数据量查询必开游标 :只要查询结果集可能超过 10 万条,务必配置useCursorFetch=true,并合理设置fetchSize
  2. fetchSize 的取值原则
    • 过小(如 100):数据库交互次数过多,性能下降;
    • 过大(如 10 万):单次拉取数据过多,仍可能触发内存压力;
    • 建议值:1000~10000,根据单条数据大小调整;
相关推荐
SQL-First布道者2 小时前
我为什么把 MyBatis 从项目中删了?
java·spring·tomcat·mybatis·mybatis plus·spring jdbc
公爵爱学习2 小时前
无人机多点导航笔记
java·前端·笔记
萧瑟余晖3 小时前
Java深入解析篇五十五之Unsafe 机制详解
java
kakawzw3 小时前
mybatis源码笔记1——JDBC和整体架构
java·mybatis
sunshine22 girl3 小时前
Java学习一 环境配置1 安装JDK,配置环境变量
java·开发语言·学习
秋饼3 小时前
Spring AI Session API 深度实战:从 ChatMemory 平滑迁移到事件溯源的企业级短期记忆
java·ai·技术分享·后端开发
QCodingDev3 小时前
Spring AI 2.0企业级RAG实战:引用校验、无依据拒答与知识治理怎么做?
java·人工智能·spring·ai
吴声子夜歌3 小时前
ApacheCommons——commons-math3(科学计算与线性代数)(一)
java·线性代数·算法·apache
砚底藏山河3 小时前
行情工程实战 M01|存储选型 CSVSQLiteMySQL
java·python·金融·maven
名字还没想好☜3 小时前
Java 用 LinkedHashMap 三行实现 LRU 缓存:accessOrder、removeEldestEntry 与线程安全
java·后端·安全·spring·缓存