单条无limit、无where的sql触发全表扫描并加载全部500万行至jvm堆内存,是典型的查询型oom;需通过应用堆栈、慢查日志rows_examined/rows_sent、sending data状态三处信号快速确认,并立即加limit止血、建索引优化、启用流式查询防控。

直接锁定问题根源:单条 SQL 无 LIMIT、无 WHERE,触发全表扫描并把全部 500 万行结果一次性加载进 JVM 堆内存,是典型的“查询型 OOM”——不是内存泄漏,而是瞬时内存需求远超堆上限。
一、快速确认是否为该类 SQL 引发的 OOM
不翻完整日志,只抓三处关键信号:
- 应用日志中出现 java.lang.OutOfMemoryError: Java heap space,且紧邻其上的堆栈含
queryForList、executeQuery、MysqlIO.readSingleRowSet等 JDBC 数据读取方法; - MySQL 慢查日志(slow_query_log)里存在执行时间短但 Rows_examined 极高(如 500w+)、Rows_sent 同样为 500w+ 的语句;
- 用
SHOW PROCESSLIST或performance_schema.threads观察到某连接长时间处于Sending data状态,且 Info 字段显示的是无条件 SELECT 语句。
二、定位肇事 SQL 的实操路径
分三步向下穿透,避免陷入底层异常细节:
-
看业务入口:从报错堆栈最上层的 Controller 或 Service 方法名入手(如
expireHoles、syncAllUsers),确认是哪个业务动作触发; -
查 DAO 层调用:找到对应方法中使用的 SQL 模板或 Mapper XML,检查是否硬编码了
SELECT * FROM xxx; - 验实际执行语句:开启 MySQL 的 general_log(临时),或在应用侧加日志打印最终拼接出的完整 SQL,确认它确实不含 WHERE、ORDER BY、LIMIT 任何约束。
三、立即止血与长效防控
线上优先保服务,再堵漏洞:
-
紧急止血:在应用代码中对该 SQL 强制增加
LIMIT 1000(或按业务允许的最大值),并加 try-catch 包裹,防止再次崩掉整个服务线程; -
SQL 层加固:在 MySQL 配置中设置
max_allowed_packet合理上限(防大结果集传输),并启用sql_safe_updates=ON(虽不直接限制 SELECT,但可联动规范开发习惯); -
开发侧卡点:在 CI 流程中加入 SQL 静态扫描规则,自动拦截无 WHERE + 无 LIMIT 的 SELECT 语句;在 MyBatis XML 中统一使用
<if test="page != null">LIMIT #{page.size}</if>模板,禁止裸写 SELECT; -
替代方案落地:对确需全量导出的场景,改用流式查询(MyBatis 的
Cursor、JDBC 的setFetchSize(Integer.MIN_VALUE))或分页拉取(WHERE id > ? ORDER BY id LIMIT 1000),避免内存驻留。
四、顺手检查 MySQL 是否已因该查询受伤
这类查询不仅压垮应用,也极易拖垮数据库本身:
- 执行
EXPLAIN查看该 SQL 的 type 是否为ALL(全表扫描),若是,立刻建索引(如CREATE INDEX idx_status ON table_name(status)); - 检查
SHOW GLOBAL STATUS LIKE 'Created_tmp_tables'是否突增——说明正在大量生成内存临时表,进一步加剧内存压力; - 观察
innodb_buffer_pool_pages_free是否持续走低,确认 Buffer Pool 是否被大结果集缓存挤占,影响其他查询性能。










