1. MyBatis游标Cursor的核心价值与常见误区第一次接触MyBatis游标时我和大多数开发者一样踩过坑。记得当时处理一个百万级数据导出需求满心欢喜地用了Cursor结果内存直接爆掉最后不得不改用分页查询。后来才发现问题出在配置上——不是Cursor没用而是我们没用对。游标的本质是流式数据处理就像用吸管喝饮料传统List查询是把整杯水一口吞下而Cursor是随喝随取。但MySQL驱动默认会把结果集完整缓存到内存这就是为什么很多人发现Cursor和List查询内存占用差不多。真正的关键在于fetchSize Integer.MIN_VALUE这个魔法参数它告诉JDBC驱动请用流式传输别缓存结果。常见配置误区主要有三个迷信FORWARD_ONLY参数测试表明这个参数对内存优化几乎无效因为MyBatis源码中根本没有相关处理逻辑不设置fetchSize默认情况下MySQL驱动会缓存整个结果集游标只是遍历方式不同忽略数据库差异Oracle/PostgreSQL等数据库的流式机制与MySQL不同需要针对性配置2. 百万级数据实战测试揭秘为了验证不同配置的实际效果我设计了一套可复现的测试方案。先通过存储过程生成10万条测试数据再用UNION ALL拼接成百万级数据集。关键测试指标包括查询耗时、GC次数、内存峰值占用。2.1 传统List查询的灾难性表现Select(SELECT * FROM large_table) ListData selectAll();测试结果触目惊心耗时7833ms触发GC 21次内存占用885MB限制500MB堆内存时直接OOM这就像用卡车运沙子——不管用不用得到先把整个沙场搬过来。当数据量达到千万级时这种写法必然导致服务崩溃。2.2 游标的基础用法对比Select(SELECT * FROM large_table) CursorData selectAll();不配置fetchSize时内存降至428MB比List减少52%GC次数仍为21次200MB内存限制下查询卡死说明单纯使用Cursor不配置fetchSize只是减少了结果集转换的内存消耗并未实现真正的流式处理。2.3 关键配置的突破性效果Select(SELECT * FROM large_table) Options(fetchSize Integer.MIN_VALUE) CursorData selectAll();配置生效后内存占用骤降至16MB下降98%GC次数减少到12次50MB内存限制下稳定运行这个配置就像给数据装上了流水线让记录一条条通过网络传输而不是整车整船地运输。实际项目中我用这个方案成功处理过单次2.3GB的报表导出需求。3. 深度解析fetchSize的底层机制为什么Integer.MIN_VALUE这个特殊值能触发流式传输追踪MySQL Connector/J源码可以发现关键逻辑// StatementImpl类 protected boolean createStreamingResultSet() { return (resultSetType FORWARD_ONLY) (resultSetConcurrency CONCUR_READ_ONLY) (fetchSize Integer.MIN_VALUE); }三个条件必须同时满足结果集类型为FORWARD_ONLY默认已满足并发模式为READ_ONLY默认已满足fetchSize等于Integer.MIN_VALUE需要显式设置这解释了为什么单独设置FORWARD_ONLY无效——它只是必要条件而非充分条件。不同数据库驱动实现有差异MySQL需要这个特殊值PostgreSQL支持任意负值Oracle需要配合TYPE_FORWARD_ONLY4. 生产环境优化实践指南4.1 内存与GC的平衡艺术测试发现一个有趣现象当堆内存限制为10MB时内存占用仅7.8MB但GC时间占比高达87%总耗时增长10倍建议设置经验常规服务保持100-200MB缓冲空间专用导出服务50MB足够处理百万级数据绝对内存限制不低于20MB4.2 多数据库适配方案// MySQL专属配置 Options(fetchSize Integer.MIN_VALUE) // PostgreSQL通用配置 Options(fetchSize -100, resultSetType FORWARD_ONLY) // Oracle适配方案 Options(fetchSize 1, resultSetType FORWARD_ONLY)建议为不同数据库编写独立的Mapper方法通过环境变量动态选择。我曾在一个混合环境中用这种方案将内存消耗从GB级降到MB级。4.3 异常处理要点流式查询需要特别注意必须使用try-with-resources确保关闭Cursor网络中断会导致ResultSet不可用超时设置要兼顾单条处理耗时try (CursorData cursor mapper.selectAll()) { cursor.forEach(item - { if (System.currentTimeMillis() - start TIMEOUT) { throw new TimeoutException(); } process(item); }); } catch (IOException e) { logger.error(流式查询异常, e); }5. 真实业务场景下的性能对比在电商订单导出场景中我们对三种方案进行了压测方案10万数据100万数据500万数据传统分页查询12s内存溢出-普通Cursor3.2s38s内存溢出流式Cursor2.8s29s143s内存占用峰值45MB58MB62MB流式方案的优势随着数据量增长越发明显。特别是在分布式环境中内存节省直接转化为服务稳定性提升。有个坑需要注意某些连接池会缓存Statement导致流式失效建议在这种场景下使用原生JDBC连接。曾经处理过一个数据迁移项目源表有3000万条记录。最初的分页方案需要6小时改用流式Cursor后降至42分钟期间内存占用始终稳定在70MB左右。这种量级的数据处理选对方案就是生死之别。