1. 项目背景与核心痛点去年处理一个千万级数据导出需求时我的Java应用在导出到50万行左右突然崩溃监控显示堆内存直接爆了。这种大数据量导出场景下的OOM内存溢出问题是后端开发者的经典噩梦。传统分页查询在数据量超过百万时要么性能断崖式下跌要么直接让服务宕机。流式查询Streaming Query正是解决这个痛点的银弹。它像打开水龙头一样让数据流出来而不是一次性把整桶水倒进内存。以MySQL为例普通查询会把所有结果集缓存在内存而流式查询通过ResultSet的TYPE_FORWARD_ONLY和CONCUR_READ_ONLY模式让数据库服务器按需传输数据。我在金融行业的报表导出系统中实践发现同样的千万级数据内存占用从原来的4GB降到了稳定的200MB左右。2. 技术方案选型对比2.1 传统分页查询的致命缺陷常规的LIMIT offset方案在处理大数据量时存在三个硬伤深度分页性能黑洞SELECT * FROM orders LIMIT 1000000, 1000需要先扫描前100万条记录数据一致性风险分页过程中若数据变更可能导致重复或遗漏内存不可控即使分批获取ResultSet默认仍会缓存全部数据测试对比MySQL 8.01000万行数据方案耗时峰值内存备注常规分页(每页5000)23分钟3.8GB第200页后响应超时游标分页(where id)8分钟1.2GB依赖自增ID有局限性流式查询6分钟210MB内存稳定无深度分页问题2.2 主流数据库的流式实现不同数据库的流式机制各有特点MySQL需在JDBC URL添加useCursorFetchtrue并设置fetchSize实测1000-5000最佳PostgreSQL原生支持流式setFetchSize(50)即可开启OracleResultSet.TYPE_FORWARD_ONLY配合适当的fetchSizeMongoDB通过cursor.batchSize控制批次大小警告MySQL Connector/J 8.0之前版本存在一个坑——即使设置了fetchSize如果没有在事务中执行会退化为全量加载。解决方案是添加Transactional(readOnlytrue)注解。3. Spring Boot实战实现3.1 核心配置示例RestController public class ExportController { GetMapping(/export/csv) public void exportLargeData(HttpServletResponse response) throws Exception { response.setContentType(text/csv); OutputStream out response.getOutputStream(); // 关键配置使用流式查询 String sql SELECT * FROM million_data_table; jdbcTemplate.setFetchSize(1000); try (Connection conn dataSource.getConnection(); PreparedStatement stmt conn.prepareStatement( sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); ResultSet rs stmt.executeQuery()) { CSVPrinter printer new CSVPrinter(new OutputStreamWriter(out), CSVFormat.DEFAULT); ResultSetMetaData meta rs.getMetaData(); // 写入CSV头 for (int i 1; i meta.getColumnCount(); i) { printer.print(meta.getColumnName(i)); } printer.println(); // 流式处理 while (rs.next()) { for (int i 1; i meta.getColumnCount(); i) { printer.print(rs.getObject(i)); } printer.println(); // 每1万行flush一次 if (rs.getRow() % 10000 0) { printer.flush(); } } } } }3.2 性能优化关键参数fetchSize的黄金区间根据实测MySQL在1000-5000之间性能最佳。过小导致网络往返频繁过大失去流式意义JVM缓冲区调优添加-Dsun.net.client.defaultReadTimeout60000防止大结果集超时连接池特殊配置HikariCP需要设置maxLifetime小于数据库wait_timeout4. 生产环境避坑指南4.1 典型问题排查表现象根本原因解决方案流式查询变全量加载MySQL非事务模式退化添加Transactional(readOnlytrue)导出文件不完整响应被提前关闭确保try-with-resources完整执行内存仍缓慢增长结果集处理代码持有引用避免在循环内积累对象数据库连接耗尽流式查询占用连接时间过长使用连接池合理超时设置4.2 内存监控技巧在Linux环境用如下命令实时观察内存变化while true; do jcmd pid VM.native_memory summary | grep -A10 Java Heap sleep 2 done如果发现内存持续增长重点检查是否在循环内创建了大对象如Date格式化器是否误用了第三方库的缓存如Jackson ObjectMapper导出文件是否及时flush避免缓冲区堆积5. 高级应用场景扩展5.1 分布式环境下的挑战当数据分散在多个分片时简单的流式查询无法满足需求。我们的解决方案是为每个分片建立独立的流式通道使用PriorityQueue实现多路归并排序通过背压机制如RxJava控制消费速度// 多数据源归并示例 ListResultSet streams shards.stream() .map(shard - shard.executeStreamingQuery()) .collect(Collectors.toList()); PriorityQueueRow heap new PriorityQueue(Comparator.comparing(Row::getSortKey)); for (ResultSet rs : streams) { if (rs.next()) { heap.add(new Row(rs)); } } while (!heap.isEmpty()) { Row current heap.poll(); writeToExport(current); // 从对应分片补充下一个元素 ResultSet src current.getSource(); if (src.next()) { heap.add(new Row(src)); } }5.2 混合导出策略对于超大规模数据亿级以上我们采用混合策略冷数据提前用Spark生成Parquet文件存储在OSS热数据实时流式查询最终合并使用AWS S3 Select或阿里云OSS Select快速拼接这种方案在某电商平台的订单导出中将原本需要4小时的导出缩短到15分钟完成。