上个月排查线上一个导出接口的故障,印象特别深。订单表数据逼近千万量级后,导出模块一次性把全量结果集捞进Java堆内存,直接触发了OOM,服务半夜重启两次。后来我把查询改成MySQL游标配合JDBC流式读取的写法,堆内存稳定控制在200MB以内,同样规模的导出任务从原来的七八分钟压到三分半,GC也平稳了。这篇文章就把这次优化涉及到的游标原理、两种落地方式、参数配置和踩过的坑完整整理一遍。如果你是做Java后端、经常要处理大结果集同步、导出、批处理的场景,这篇文章值得看完再动手改代码。
1. 项目背景与问题本质:大结果集一次性加载为什么扛不住
很多团队一开始写大查询都是最直觉的方式,执行一个select把结果全部装进List,然后foreach处理。数据量在几十万条的时候问题不大,一旦到了几百万、上千万,这个写法的所有隐患都会集中爆发。
1.1 从一次OOM事故说起
当时导出模块的伪代码大概长这样:
java复制public void exportOrders(LocalDate start, LocalDate end) {
List<OrderDO> orders = orderMapper.selectByTime(start, end);
for (OrderDO order : orders) {
// 生成Excel行 / 推送下游 / 写临时文件...
}
}
selectByTime这个查询在MySQL里能正常执行,索引也没问题。问题出在结果集本身:一千万订单不是一次性全部返回,但MySQL JDBC驱动默认行为是等查询执行完毕后,把全部结果一次性从服务端拉到客户端内存里。JVM堆给了2G,光这一个List就可能吃掉1.5G,再叠加其他接口流量,OOM几乎是必然的。
服务端不停Full GC、接口响应越来越慢、最后直接OutOfMemoryError,这个故障链路在监控里看得清清楚楚。事后复盘时我们意识到,问题不在SQL本身,而在“数据搬运方式”——一次性搬运的管道太粗,内存这个蓄水池装不下。
1.2 为什么分页查询替代不了游标
有人会说,既然一次性加载不行,那把大结果集拆成 limit 分页不就行了?我在项目里确实先试过这个方案,但很快发现了几个更深层的坑。
第一个坑是深度翻页性能退化。LIMIT 1000000, 1000 这类深分页,MySQL需要先扫描一千万行,再丢弃前面九百九十九万九千行,才能返回最后那一千行。这个成本随着页码变大线性上升,导出任务越跑越慢。线上实测50万条数据分页导出,用offset方式跑了近12分钟,其中有8分钟都耗在深翻页扫描上。
第二个坑是一致性问题。导出任务往往要好几分钟,如果业务表在导出过程中有新增、删除、状态变更,基于偏移量的分页很容易出现重复行或者漏数据。比如排序列上有新数据插入,分页游标会偏移错位,某些行就被重复导出或跳过。要解决一致性问题就得开启事务并锁表或快照,代价反而更大。
第三个坑是连接和事务占用。分页方案虽然不占内存,但长时间占用一个数据库连接反复执行查询,连接池里的连接被“长期持有”,在高并发场景下很容易拖垮整个连接池。
所以分页方案只适合“给用户看的前台分页”这种小结果集场景,不适合“后台批量处理大数据集”的场景。这时候游标方案的价值就体现出来了:服务端一边查,客户端一边读,内存里永远只保留一小批数据,数据一致性靠一个事务getSnapshot完成,连接占用也只是一次查询的时长。用生活中的例子理解,一次性加载像用一个大水缸把整条河的水先存放好再慢慢用,游标像把水管直接接到客户端,开水龙头边流边接,水缸只需要小小一个就行。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 游标方案设计:MySQL游标的工作机制与适用边界
MySQL里的“游标”这个词其实会在两个层面出现:一个是存储过程里的CURSOR,另一个是JDBC驱动通过服务端游标实现的流式读取。两者底层机制相通,但使用场景差别很大。新手容易混淆,我先把原理讲清楚。
2.1 游标的工作机制
游标本质上是“客户端与MySQL服务端之间的一条数据流水线”。服务端执行完查询后,并不会拼命把所有结果一口气丢给客户端,而是维护一个结果集游标指针,客户端按需调用fetch指令,服务端才把游标指向的下一批数据发送过来。
可以类比成外卖配送:一次性加载是商家把一百份餐全塞进你的后座,后座装不下就洒了;游标是配送员一份一份(或一批一批)递给你,你手里始终只有一两份餐的量,全吃完再接下一份。
MySQL中游标有几个特性需要注意:只读(不能通过游标修改数据)、不可滚动(只能向前遍历,不能回退到上一行)、敏感度取决于实现(JDBC流式读取的敏感度是ASENSITIVE,结果集快照是否实时取决于语句类型)。这些特性决定了游标适合顺序遍历批处理,不适合随机访问。
2.2 使用原则:什么时候该用游标,什么时候别用
我自己总结了一个简单的判断标准:如果结果集可能超过JVM堆可用内存的十分之一,或者单条记录较大且总条数超过几十万,就别再想着一次性加载了。 具体适合游标的场景包括:
- 大数据量导出文件(
