踩坑记: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=true和defaultFetchSize=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,根据单条数据大小调整;
相关推荐
IT枫斗者枫哥38 分钟前
MyBatis一对多分页:LIMIT 20,为什么凑不齐20个订单?
java·数据库
代码方舟38 分钟前
Java数据工程:利用天远全网运营商三要素优化线上实名认证合规体验
java·人工智能
BBmmo39 分钟前
我的 Java 学习笔记 · 第 7 篇:泛型与通配符
java
独泪了无痕39 分钟前
Hutool之RandomUtil:随机数生成的终极利器
java·后端
Flynt39 分钟前
"MySQL搜不动就上ES"?我先在50万行数据上测了它自带的ngram全文索引
mysql·elasticsearch·搜索引擎
花开路口40 分钟前
深入理解 Java/Kotlin 协变与逆变
java·kotlin
运行时异常40 分钟前
【WMS 仓储系统集成 AI Agent 实战】第 7 讲:Vue3 前端工程化——Token 刷新锁、Markdown 渲染踩坑与权限体系
java
她的男孩41 分钟前
开放接口限流从 20 改到 200 还是每分钟 20 次:拆完防重放+幂等+限流,我找到 5 个静默失效的坑
java·后端·架构
天天被压力1 小时前
【Python 量化取数指南 #13】Python 把行情落库:sqlite 一键存,回测随用随取
java·人工智能·python
天天被压力1 小时前
【Python 量化取数指南 #14】Python 清洗行情数据:复权停牌对齐,回测不翻车
java·人工智能·python