JDBC批量查询语句 setFetchSize不生效问题分析

背景

一个简单的基于 JDBC 采集数据库表的功能,当采集 Postgre SQL 某表,其数据量达到 500万左右的时候,程序一启动就将 JVM 堆内存「6G」干满了。

问题是程序中使用了游标的只前进配置,且设置了 fetchSize 属性:

c 复制代码
queryStat = connection.prepareStatement(executeSql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
queryStat.setFetchSize(batchSize);

为什么这个批量拉取数据的配置不生效呢?本文记录这个问题的排查过程及优化方法。

导出堆内存

程序一启动,jmap -heap 查看堆内存,老年代直接干到 99.98 % 了: JVM 启动最大堆内存已经调整到 6G 了,还是撑不住。感觉 SQL 查询的时候一下子将表的全部结果都加载到内存了,前面配置的批量拉取设置根本没生效。导出堆内存文件,进行分析。

c 复制代码
nohup jmap -F -dump:live,format=b,file=/home/dump-result.hprof 23055 &

堆内存太大了,只能走后台进程的方式导出,接近一个小时才导出了 dump 文件,5.8G,确实跟 JVM 最大内存一样了。

堆内存分析

使用 mat 打开这个文件,直接内存溢出了。然后修改 mat 的 JVM 参数到8G后,得到分析结果不对,才几十M,明显不符合。

有很多 unreachable object,重新修改 mat 配置,勾选 "keep unreachable objects",同时修改展示单位为 MB:

删除上次分析的结构后,重新导入 dump 文件分析,得到分析结果: 点开 Leak Suspects 查看内存泄漏的地方,发现最大的对象4.5G ,是一个列表,列表原始类型是 org.postgresql.core.Tuple ,这个类就是 JDBC 封装的查询结果 而这个类的对象总数的量级跟表记录总数一致: 少掉的那些,应该是 GC 努力回收过的,但是剩余量还是很大。

这基本验证了前面的猜测,批量查询实际上成了全量查询了 。为了再次确认,调整代码,造一张同结构、但是数据总量6万左右的表,然后在 while(result.next()) 遍历的循环里面加上 sleep 10 分钟后启动程序,导出堆内存。

这次程序老年代内存没有撑满,导出内存分析,Tuple 这个查询结果类对象的个数,跟数据库表总记录数「58000」多了21,基本可以确定这个批量size 没有生效。

问题分析

为什么批量加载不生效呢?是数据库的问题?驱动的问题?

尝试的方法:

  1. ❌升级数据库驱动为最新版本,无效。
  2. ❌在 while(result.next()) 遍历过程中,直接打印一个字符串后 continue,休眠5秒,手动调用 GC。不做任何操作,且手动触发 GC,JVM 内存还是满了。
  3. ❌怀疑数据库有问题,确定测试环境版本和出问题的现场环境一致。
  4. ❌目标数据库是基于 OpenGauss 自研的数据库,数据库有问题,不支持游标的批量获取数据怎么办?

搜到一篇文章 《Postgres查询结果集的获取方法及其优缺点》 ,里面提到了 PostgreSQL 数据库的批量获取游标结果集生效的四个条件:

  1. 连到数据库服务的连接必须是基于V3协议的,V3协议是7.4及更新版本PG才能支持的,并且是他们的默认协议;
  2. Connection必须是非自动提交模式.后端会在事务的结束的时候关闭游标,所以,在自动提交模式里,还没从游标里获取任何东西的时候,后端就已经把游标关闭了。「冷知识:Connection 默认是自动提交的。」
  3. Statement必须以ResultSet.TYPE_FORWARD_ONLY的类型来创建,该结果集类型是默认的,所以可以直接使用stmt = conn.createStatement()来创建(或者stmt = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY)).因此基于游标的结果集是只能向前获取,不能向后或者跳跃获取的。「PS:PostgreSQL默认就是这个类型,所以这个不是关键。
  4. 查询sql语句必须是一个单一的语句,不能是由分好分隔的多个语句。这个在本应用中不存在。

之前没仔细注意第2点,找了三天实在没办法了。又打开这篇文章,仔细看了一下,发现了这个点。

检查代码确实没有设置自动提交参数,加上它,还原 JVM 参数为2G,然后测试500万条数据顺利采集完成,老年代堆只占2%。

排查了三天的问题,就这么简单的一行代码就解决了吗?赶在周末之前干掉问题,确实太幸运了。

优化结果

继续优化,循环遍历数据总量到达一个值后,手动触发 GC并休眠1秒:

c 复制代码
// 手动触发GC,且休眠等待
if (count == maxFetchSize) {
    logger.info("Reach max batch size {}, sleep 1s to gc", maxFetchSize);
    count = 0;
    // 手动触发 GC
    Runtime.getRuntime().gc();
    // 等待GC完成
    Thread.sleep(1000);
}

将优化后的结果,加上 sleep 10分钟后,导出堆栈分析,发现这次 Tuple 类的个数就是 setFetchSize=2000,还多了21个。 跟上面那个一样,数据总量+21,说明额外还有 21 个对象,为查询操作提供了不为人知的功能。总归来说,只有加上这句话 connection.setAutoCommit(false); 才生效,才是真正的批量查询数据。

启示录

一开始就检索到了 《Postgres查询结果集的获取方法及其优缺点》 这篇文章,里面提到了 PostgreSQL 数据库的批量获取游标结果集生效的方法,但是忽略了重要的那个条件。

循环处理数据时,达到一个值后,手动触发 GC 还是有效的,可以让整个采集过程中老年代内存占用情况稳定在 2% 左右;如果去掉 GC 的话,内存会缓慢升至 10% 左右,但是已经不会再僵死了。

这个 JDBC 的批量查询不生效问题,前年冬天采集 Doris 的时候也发现了,只是后来没有细究。这次又碰到了,不知掉 Doris 能不能用这个配置接近呢?或者说 Doris 数据库支不支持批量查询呢?

相关推荐
无名之逆14 分钟前
hyperlane:Rust HTTP 服务器开发的不二之选
服务器·开发语言·前端·后端·安全·http·rust
机构师21 分钟前
<iced><rust><GUI>基于rust的GUI库iced的学习(02):svg图片转png
后端·rust
老赵骑摩托23 分钟前
Go语言nil原理深度解析:底层实现与比较规则
开发语言·后端·golang
卑微小文33 分钟前
惊!代理 IP 竟成社交媒体营销破局“神助攻”!
后端
程序员爱钓鱼44 分钟前
Go 语言邮件发送完全指南:轻松实现邮件通知功能
后端·go·排序算法
Cloud_.1 小时前
Spring Boot整合Redis
java·spring boot·redis·后端·缓存
海狸鼠1 小时前
几行代码实现MCP服务端/客户端(接入DeepSeek)
前端·后端
37手游后端团队1 小时前
10分钟读懂RAG技术
人工智能·后端
Moment1 小时前
岗位急招,算法实习、音乐生成、全栈、flutter 都有,早十晚六 😍😍😍
前端·后端·面试
金融数据出海2 小时前
使用Spring Boot对接印度股票数据源:实战指南
后端