JDBC游标读取失效引发内存溢出问题的排查剖析

JDBC游标读取失效引发内存溢出问题的排查解析

问题呈现

程序采用游标进行分批次读取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实现。

@Override
public java.sql.PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
    synchronized (getConnectionMutex()) {
        checkClosed();

        //
        // FIXME: Create warnings if can't create results of the given type or concurrency
        //
        ClientPreparedStatement pStmt = null;

        boolean canServerPrepare = true;

        String nativeSql = this.processEscapeCodesForPrepStmts.getValue() ? nativeSQL(sql) : sql;

        if (this.useServerPrepStmts.getValue() && this.emulateUnsupportedPstmts.getValue()) {
            canServerPrepare = canHandleAsServerPreparedStatement(nativeSql);
        }

        if (this.useServerPrepStmts.getValue() && canServerPrepare) {
            if (this.cachePrepStmts.getValue()) {
                synchronized (this.serverSideStatementCache) {
                    pStmt = this.serverSideStatementCache.remove(new CompoundCacheKey(this.database, sql));

                    if (pStmt != null) {
                        ((com.mysql.cj.jdbc.ServerPreparedStatement) pStmt).setClosed(false);
                        pStmt.clearParameters();
                    }

                    if (pStmt == null) {
                        try {
                            pStmt = ServerPreparedStatement.getInstance(getMultiHostSafeProxy(), nativeSql, this.database, resultSetType,
                                    resultSetConcurrency);
                            if (sql.length() < this.prepStmtCacheSqlLimit.getValue()) {
                                ((com.mysql.cj.jdbc.ServerPreparedStatement) pStmt).isCacheable = true;
                            }

                            pStmt.setResultSetType(resultSetType);
                            pStmt.setResultSetConcurrency(resultSetConcurrency);
                        } catch (SQLException sqlEx) {
                            // Punt, if necessary
                            if (this.emulateUnsupportedPstmts.getValue()) {
                                pStmt = (ClientPreparedStatement) clientPrepareStatement(nativeSql, resultSetType, resultSetConcurrency, false);

                                if (sql.length() < this.prepStmtCacheSqlLimit.getValue()) {
                                    this.serverSideStatementCheckCache.put(sql, Boolean.FALSE);
                                }
                            } else {
                                throw sqlEx;
                            }
                        }
                    }
                }
            } else {
                try {
                    pStmt = ServerPreparedStatement.getInstance(getMultiHostSafeProxy(), nativeSql, this.database, resultSetType, resultSetConcurrency);

                    pStmt.setResultSetType(resultSetType);
                    pStmt.setResultSetConcurrency(resultSetConcurrency);
                } catch (SQLException sqlEx) {
                    // Punt, if necessary
                    if (this.emulateUnsupportedPstmts.getValue()) {
                        pStmt = (ClientPreparedStatement) clientPrepareStatement(nativeSql, resultSetType, resultSetConcurrency, false);
                    } else {
                        throw sqlEx;
                    }
                }
            }
        } else {
            pStmt = (ClientPreparedStatement) clientPrepareStatement(nativeSql, resultSetType, resultSetConcurrency, false);
        }

        return pStmt;
    }
}

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

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

继续debug,进入canHandleAsServerPreparedStatement方法:

private boolean canHandleAsServerPreparedStatement(String sql) throws SQLException {
    if (sql == null || sql.length() == 0) {
        return true;
    }

    if (!this.useServerPrepStmts.getValue()) {
        return false;
    }

    boolean allowMultiQueries = this.propertySet.getBooleanProperty(PropertyKey.allowMultiQueries).getValue();

    if (this.cachePrepStmts.getValue()) {
        synchronized (this.serverSideStatementCheckCache) {
            Boolean flag = this.serverSideStatementCheckCache.get(sql);

            if (flag != null) {
                return flag.booleanValue();
            }

            boolean canHandle = StringUtils.canHandleAsServerPreparedStatementNoCache(sql, getServerVersion(), allowMultiQueries,
                    this.session.getServerSession().isNoBackslashEscapesSet(), this.session.getServerSession().useAnsiQuotedIdentifiers());

            if (sql.length() < this.prepStmtCacheSqlLimit.getValue()) {
                this.serverSideStatementCheckCache.put(sql, canHandle ? Boolean.TRUE : Boolean.FALSE);
            }

            return canHandle;
        }
    }

    return StringUtils.canHandleAsServerPreparedStatementNoCache(sql, getServerVersion(), allowMultiQueries,
            this.session.getServerSession().isNoBackslashEscapesSet(), this.session.getServerSession().useAnsiQuotedIdentifiers());
}

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

public static boolean canHandleAsServerPreparedStatementNoCache(String sql, ServerVersion serverVersion, boolean allowMultiQueries,
        boolean noBackslashEscapes, boolean useAnsiQuotes) {

    // Can't use server-side prepare for CALL
    if (startsWithIgnoreCaseAndNonAlphaNumeric(sql, "CALL")) {
        return false;
    }

    boolean canHandleAsStatement = true;

    boolean allowBackslashEscapes = !noBackslashEscapes;
    String quoteChar = useAnsiQuotes ? "\"" : "'";

    if (allowMultiQueries) {
        if (StringUtils.indexOfIgnoreCase(0, sql, ";", quoteChar, quoteChar,
                allowBackslashEscapes ? StringUtils.SEARCH_MODE__ALL : StringUtils.SEARCH_MODE__MRK_COM_WS) != -1) {
            canHandleAsStatement = false;
        }
    } else if (startsWithIgnoreCaseAndWs(sql, "XA ")) {
        canHandleAsStatement = false;
    } else if (startsWithIgnoreCaseAndWs(sql, "CREATE TABLE")) {
        canHandleAsStatement = false;
    } else if (startsWithIgnoreCaseAndWs(sql, "DO")) {
        canHandleAsStatement = false;
    } else if (startsWithIgnoreCaseAndWs(sql, "SET")) {
        canHandleAsStatement = false;
    } else if (StringUtils.startsWithIgnoreCaseAndWs(sql, "SHOW WARNINGS") && serverVersion.meetsMinimum(ServerVersion.parseVersion("5.7.2"))) {
        canHandleAsStatement = false;
    } else if (sql.startsWith("/* ping */")) {
        canHandleAsStatement = false;
    }

    return canHandleAsStatement;
}

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(默认设置下会影响到需要多语句执行的场景,可根据实际需要临时开启)。

全文完。

文章整理自互联网,只做测试使用。发布者:Lomu,转转请注明出处:https://www.it1024doc.com/13058.html

(0)
LomuLomu
上一篇 2025 年 8 月 5 日
下一篇 2025 年 8 月 5 日

相关推荐

  • 无需邀请码免费申领datagrip激活码,实用破解教程

    声明:本文所涉及的 DataGrip 破解补丁与激活码均源自网络公开资源,仅供个人学习研究,禁止商用。若条件允许,请支持正版! 先放一张“战绩图”——DataGrip 2025.2.1 已成功激活到 2099 年,爽翻! 下面用图文手把手带你完成最新版 DataGrip 的激活流程。 嫌折腾?直接入手官方正版,全家桶账号低至 32 元/年,登录即用:http…

    DataGrip激活码 2025 年 10 月 22 日
    33600
  • IDEA 破解教程完整版本

    IDEA 2025.2.1破解教程:最新激活码+补丁下载(图文详解) 重要提示:本文所涉及的IDEA破解补丁与激活码资源均来源于网络收集,严禁用于商业用途,仅限个人学习研究使用。如内容存在侵权问题,请联系本人删除。经济条件允许的话,强烈建议支持官方正版! 话不多说,先展示IDEA 2025.2.1版本破解成功的实况截图。如图所示,激活有效期已延伸至2099年…

    IDEA破解教程 2026 年 1 月 5 日
    22800
  • IDEA 激活码每次都失效?教你找到真正可用的方法

    申明:本教程 IntelliJ IDEA 破解补丁、激活码均收集于网络,请勿商用,仅供个人学习使用,如有侵权,请联系作者删除。若条件允许,希望大家购买正版 ! IDEA是 JetBrains 推出的开发编辑器,功能强大,适用于 Windows、Mac 和 Linux 系统。本文将详细介绍如何通过破解补丁实现永久激活,解锁所有高级功能。 不管你是什么版本、什么…

    IDEA破解教程 2026 年 4 月 9 日
    14300
  • 最新datagrip激活码获取方式+破解工具

    DataGrip破解教程:2025最新永久激活码+补丁下载(完整版) 重要声明:本文所涉及的DataGrip破解补丁与激活码均来源于网络收集,仅限个人学习研究使用,严禁任何商业用途。若内容存在侵权问题,请联系本人删除。经济条件允许的情况下,强烈建议购买官方正版授权,支持软件开发者。如需正版授权,可访问官方渠道购买,支持JetBrains全家桶产品,年费低至3…

    DataGrip激活码 2026 年 1 月 9 日
    32800
  • 2025最新PyCharm永久激活码分享|PyCharm破解教程(亲测有效)💻

    本教程适用于Jetbrains全家桶,包括IDEA、PyCharm、DataGrip、Golang等所有产品!✨ 先给大家看看最新PyCharm版本破解成功的截图,有效期直接到2099年,简直不要太爽!😎 下面我将用详细的图文教程,手把手教你如何激活PyCharm到2099年。这个方法同样适用于旧版本哦~ 无论你是Windows、Mac还是Linux用户 无…

    PyCharm激活码 2025 年 6 月 18 日
    1.9K00

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信