1. 大数据量查询为何成为性能瓶颈当数据量达到百万级甚至千万级时传统的ORM查询方式会暴露出明显的性能问题。最近在重构一个订单统计系统时我遇到了一个典型场景需要导出近3个月的所有订单明细约450万条记录使用MyBatisPlus的常规查询方法接口响应时间超过了8分钟这显然无法接受。1.1 全量加载的内存消耗问题MyBatisPlus默认的selectList()方法会一次性将所有结果加载到JVM内存中。假设每条订单记录占用约2KB内存450万条数据将消耗4,500,000 × 2KB ≈ 8.6GB这直接导致频繁的Full GC发生服务响应延迟飙升严重时引发OOM导致服务崩溃1.2 结果集传输的带宽瓶颈数据库驱动默认会将完整结果集传输到应用层。在分布式架构中如果数据库与应用服务器跨机房部署网络传输可能成为瓶颈。实测发现在50Mbps带宽下传输450万条记录需要(8.6GB × 8) / 50Mbps ≈ 1376秒 ≈ 23分钟1.3 传统分页查询的局限性常见的分页优化方案如LIMIT offset, size在大偏移量时效率骤降。以MySQL为例当执行LIMIT 4000000, 1000时-- 实际执行的是全表扫描到400万行后取1000条 EXPLAIN SELECT * FROM orders LIMIT 4000000, 1000;执行计划显示需要扫描4,001,000行而不仅仅是需要的1000行。2. MyBatisPlus的流式查询解决方案2.1 基于ResultHandler的流式处理MyBatis原生支持通过ResultHandler逐行处理结果。MyBatisPlus在此基础上进行了封装Mapper public interface OrderMapper extends BaseMapperOrder { Select(SELECT * FROM orders WHERE create_time BETWEEN #{start} AND #{end}) void streamQuery(Param(start) Date start, Param(end) Date end, ResultHandlerOrder handler); } // 调用示例 orderMapper.streamQuery(startDate, endDate, resultContext - { Order order resultContext.getResultObject(); // 单条记录处理逻辑 processOrder(order); });关键配置项mybatis-plus: configuration: default-fetch-size: 100 # 控制每次从数据库获取的批次大小2.2 游标查询的JDBC层实现对于更底层的控制可以直接使用JDBC的游标特性try (SqlSession sqlSession sqlSessionFactory.openSession(ExecutorType.SIMPLE)) { OrderMapper mapper sqlSession.getMapper(OrderMapper.class); try (CursorOrder cursor mapper.scan(startDate, endDate)) { cursor.forEach(order - { // 处理单条记录 if (order.getAmount() 10000) { processVipOrder(order); } }); } }重要提示必须使用try-with-resources确保游标关闭否则会导致数据库连接泄漏2.3 性能对比测试在相同450万条数据场景下的测试结果查询方式内存占用执行时间适用场景传统selectList8.6GB8min23s小数据量即时处理ResultHandler流式50MB6min12s需要逐行业务逻辑处理游标查询30MB5min47s简单遍历和批量导出3. 实战中的优化技巧3.1 合理设置fetchSize参数数据库驱动的fetchSize决定了每次网络往返获取的行数。Oracle和MySQL的默认值差异较大// MySQL优化建议基于Connector/J jdbc:mysql://host:3306/db?useCursorFetchtruedefaultFetchSize1000 // PostgreSQL配置 jdbc:postgresql://host:5432/db?defaultRowFetchSize500实测不同fetchSize对性能的影响单位秒fetchSizeMySQLPostgreSQL100347412500289315100025627850002332413.2 结果集处理并行化结合Java的并行流提高处理效率try (CursorOrder cursor mapper.scan()) { cursor.stream() .parallel() .forEach(order - { // 线程安全的处理逻辑 safeProcess(order); }); }注意事项需要确保处理逻辑是线程安全的合理控制并行度避免过度消耗线程池资源3.3 字段优化与延迟加载对于包含大字段的表可以采用部分加载策略Select(SELECT id, order_no, amount FROM orders WHERE create_time BETWEEN #{start} AND #{end}) void streamBasicInfo(Param(start) Date start, Param(end) Date end, ResultHandlerOrder handler); // 需要时再查询详情 Select(SELECT * FROM orders WHERE id #{id}) Order getDetail(Param(id) Long id);4. 典型问题排查指南4.1 游标超时问题错误现象ERROR: cursor SQL_CUR0x1a3b5c does not exist解决方案增加JDBC超时配置spring: datasource: hikari: connection-timeout: 60000 validation-timeout: 5000调整数据库游标超时时间以PostgreSQL为例SET statement_timeout 60000;4.2 内存泄漏排查诊断步骤使用jmap生成堆转储jmap -dump:live,formatb,fileheap.hprof pid分析工具如MAT查找Retained Heap最大的对象检查未关闭的ResultSet或Statement4.3 分库分表下的流式查询在ShardingSphere等分库分表场景中需要特殊处理// 需要关闭流式结果合并 spring: shardingsphere: props: max.connections.size.per.query: 5 sql.show: true5. 进阶场景解决方案5.1 与批处理框架集成结合Spring Batch处理超大数据集Bean public ItemReaderOrder orderReader() { return new MyBatisCursorItemReaderBuilderOrder() .sqlSessionFactory(sqlSessionFactory) .queryId(com.example.mapper.OrderMapper.scan) .build(); }5.2 结果集写入优化使用Apache Commons CSV实现高效导出try (CursorOrder cursor mapper.scan(); CSVPrinter printer new CSVPrinter(new FileWriter(orders.csv), CSVFormat.DEFAULT)) { cursor.forEach(order - { printer.printRecord( order.getId(), order.getOrderNo(), order.getAmount() ); if (recordsCount.getAndIncrement() % 10000 0) { printer.flush(); } }); }5.3 监控与性能分析通过Micrometer监控流式查询指标Metrics.gauge(order.stream.records, recordsCount); Timer timer Metrics.timer(order.processing.time); cursor.forEach(order - { timer.record(() - processOrder(order)); });