暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

JDBC游标读不生效导致OOM问题排查分析

GreatSQL社区 2025-07-11
112

* GreatSQL社区原创内容未经授权不得随意使用,转载请联系小编并注明来源。


问题描述

程序使用游标读分批读取MySQL的数据,但是程序容器却发生OOM

基本信息

MySQL版本:8.0.25

JDBC版本:8.0.25

JDBC配置:

connectionProperties=useUnicode=true;autoReconnect=true;defaultFetchSize=800;useServerPrepStmts=false;rewriteBatchedStatements=true;useCompression=true;useCursorFetch=true;allowMultiQueries=true

批量程序的OOM日志:


问题分析

获取dump下来的内存快照后,使用jdk自带的Java visualVM打开后,找到右侧最大的对象:



发现java.lang.Object[]
最大,点击后发现里面存的是ByteArrayRow
类型对象,它是数据库的游标对象,说明在查询数据库的过程中,内存已经溢出,还没来得及转换成实体类,说明此时游标读失效。

通过查看堆栈上的线程报错信息


显示的代码的流程调用的是ClientPreparedStatement
类的方法,没有调用ServerPreparedStatement
类的方法,调用的是客户端来执行,此时是普通读。

利用游标读demo测试,发现游标读的调用时走ServerPreparedStatement
类的方法(下图第3、4行),然后调用ServerPreparedQuery
类的ServerPreparedQuery
方法(下图第1行)

查看源码,ServerPreparedQuery
方法中调用了packet.writeInteger(IntegerDataType.
INT1

OPEN_CURSOR_FLAG
)
方法进行游标读。

ClientPreparedStatement
:查询是在客户端准备的。这意味着所有的SQL语句处理,包括参数替换,都在客户端完成,然后作为一个整体发送到服务器,只能普通读。

ServerPreparedStatement
:查询是在服务器端准备的。这意味着SQL语句和其参数在服务器上被处理,这可以利用服务器的某些优化特性,可以普通读、游标读、流式读。

进一步分析,PreparedStatement的具体实现什么时候确定是ClientPreparedStatement还是ServerPreparedStatement?

在调用Connection.prepareStatement()
Connection.prepareStatement(String sql, int resultSetType, int resultSetConcurrency)
等方法时,JDBC驱动会根据当前的配置和数据库服务器的能力来确定使用哪种PreparedStatement实现。


通过debug发现,会走到16行的 canServerPrepare = canHandleAsServerPreparedStatement(nativeSql);

说明在jdbc配置useServerPrepStmts=true
是生效的,emulateUnsupportedPstmts
系统默认值就是true,判断成立。


继续debug,进入canHandleAsServerPreparedStatement
方法


cachePrepStmts
默认值是false,前面的判断是不成立的,直接走到最后的StringUtils
类的*canHandleAsServerPreparedStatementNoCache
*方法。


canHandleAsServerPreparedStatementNoCache是在不开启缓存的情况下是否能使用ServerPreparedStatement

根据后续反馈,游标读不是一直不生效,只是在运行某个sql的时候不生效,为了隐私,这里将这个sql简化为:

SELECT * FROM t;

由于sql不是CALL开头而且jdbc的参数allowMultiQueries=true
会走到15行的代码,indexOfIgnoreCase
方法的意思是在字符串中查找子字符串的位置,忽略大小写,并有选择地跳过由给定标记限定的文本或在注释中的文本。

这行的代码意思在sql语句中查找;的位置,忽略''符号之间的内容,如果不存在,即返回-1,就允许使用ServerPreparedStatement
,否则使用ClientPreparedStatement
。经过debug,确实会走到这里。


问题总结

问题发生路径:开启allowMultiQueries=true
且当前sql带分号 ——>

canHandleAsServerPreparedStatementNoCache
返回值为false ——>

canHandleAsServerPreparedStatement
返回值为false  ——>

执行 (ClientPreparedStatement) clientPrepareStatement(nativeSql, resultSetType, resultSetConcurrency, false)
返回ClientPreparedStatement
 ——>客户端执行普通读。

使用建议

  1. 书写SQL时去掉后面的分号;

  2. 不要开启allowMultiQueries=true
    ,其默认值为false默认设置下会影响到需要多语句执行的场景,可根据实际需要临时开启)。

Enjoy GreatSQL :)

<往 期 推 荐>
MOD函数索引实战:解决百万级数据分批处理性能瓶颈
GreatSQL通过伪装从库回放Binlog文件
GreatSQL社区月报 | 2025.6

《用三分钟学会一个MySQL知识》

<关于 GreatSQL>

GreatSQL数据库是一款开源免费数据库,可在普通硬件上满足金融级应用场景,具有高可用、高性能、高兼容、高安全等特性,可作为MySQL或Percona Server for MySQL的理想可选替换。

💻社区官网: https://greatsql.cn/ 
Gitee  https://gitee.com/GreatSQL/GreatSQL
GitHub  https://github.com/GreatSQL/

🆙BiliBili  : https://space.bilibili.com/1363850082

(对文章有疑问或见解可去社区官网提出哦~)

加入微信交流群
加入QQ交流群
想看更多技术好文,点个"在看"吧!

文章转载自GreatSQL社区,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论