Java实现JDBC大结果集流式写入JSONL文件的方案咨询
解决Java流式处理超大SQL结果集转JSONL的内存溢出问题
哥们,你这个问题太典型了——Java处理超大SQL结果集最容易踩的就是「全量加载进内存」的坑,尤其是你用MapListHandler的时候,它直接把所有行都塞进List<Map>里,10GB的数据肯定直接爆堆。咱们直接上可行的解决方案,核心思路就是流式逐行处理,绝不把整个结果集留在内存里。
核心问题分析
你之前的代码用MapListHandler会把查询结果全部加载到内存中,哪怕你之后逐行写入并flush,内存里的List<Map>还是会一直存在,直到整个循环结束才会被GC回收。这也是为什么小数据集没问题,大数据集直接爆堆的原因。另外,SQL Server默认会把全量结果拉到客户端内存,哪怕你设置了fetchSize也没用,这是另一个容易忽略的坑。
解决方案:自定义流式Handler + 逐行写入
我们需要修改查询逻辑,放弃MapListHandler,改用ResultSetHandler自定义流式处理逻辑,同时配置SQL Server的游标模式,让数据库真正逐批次返回结果。
1. 修改SQL客户端的查询方法,支持流式处理
把原来返回List<Map>的方法改成接受一个Consumer来逐行处理数据,这样每一行处理完就可以被GC回收:
import org.apache.commons.dbutils.QueryRunner; import org.apache.commons.dbutils.ResultSetHandler; import org.apache.commons.dbutils.StatementConfiguration; import org.apache.commons.dbutils.handlers.BasicRowProcessor; import org.apache.commons.dbutils.RowProcessor; import java.util.Map; import java.util.function.Consumer; import java.sql.ResultSet; public void queryStream(String queryText, Consumer<Map<String, Object>> rowConsumer) { try { DbUtils.loadDriver("com.microsoft.sqlserver.jdbc.Driver"); DataSource ds = this.initDataSource(); // 关键配置:设置fetchSize,同时确保SQL Server用游标模式返回结果 StatementConfiguration sc = new StatementConfiguration.Builder() .fetchSize(10000) // 每次从数据库拉取10000行,可根据内存调整 .build(); QueryRunner queryRunner = new QueryRunner(ds, sc); // 自定义ResultSetHandler,逐行处理结果 ResultSetHandler<Void> streamingHandler = rs -> { RowProcessor rowProcessor = new BasicRowProcessor(); // DbUtils默认行处理器,把ResultSet行转成Map while (rs.next()) { Map<String, Object> row = rowProcessor.toMap(rs); rowConsumer.accept(row); // 每处理10000行flush一次,减少文件IO缓冲压力 if (rs.getRow() % 10000 == 0) { if (rowConsumer instanceof JsonLOutputWriter) { ((JsonLOutputWriter) rowConsumer).flush(); } } } return null; }; queryRunner.query(queryText, streamingHandler); } catch (Exception e) { logger.error("流式查询失败", e); throw new RuntimeException("处理超大结果集出错", e); // 不要返回null,抛出异常让上层处理 } }
2. 优化JsonLOutputWriter,支持流式消费
让Writer实现Consumer接口,同时改用BufferedWriter提升IO效率,并且自动批量flush:
import com.google.gson.Gson; import com.google.gson.GsonBuilder; import java.io.*; import java.util.Map; import java.util.function.Consumer; public class JsonLOutputWriter implements Consumer<Map<String, Object>> { private final Gson gson; private final BufferedWriter writer; private static final String ENCODING = "UTF-8"; private static final int FLUSH_BATCH_SIZE = 10000; private int rowCount = 0; public JsonLOutputWriter(String filename) throws IOException { GsonBuilder gsonBuilder = new GsonBuilder(); gsonBuilder.serializeNulls(); this.gson = gsonBuilder.create(); // 用BufferedWriter,设置8KB缓冲区,平衡IO性能和内存占用 this.writer = new BufferedWriter( new OutputStreamWriter(new FileOutputStream(filename), ENCODING), 8192 ); } @Override public void accept(Map<String, Object> row) { try { writer.write(gson.toJson(row)); writer.newLine(); rowCount++; // 批量flush,避免频繁IO操作 if (rowCount % FLUSH_BATCH_SIZE == 0) { flush(); } } catch (IOException e) { throw new UncheckedIOException("写入JSONL行失败", e); } } public void flush() throws IOException { writer.flush(); } // 必须关闭资源,确保最后一批数据写入磁盘 public void close() throws IOException { flush(); writer.close(); } }
3. 主方法调用(用try-with-resources确保资源释放)
public static void main(String[] args) { String inputSql = "SELECT * FROM your_large_table"; String outputFile = "result.jsonl"; try (JsonLOutputWriter writer = new JsonLOutputWriter(outputFile)) { YourSqlClient client = new YourSqlClient(); client.queryStream(inputSql, writer); } catch (IOException e) { // 处理文件IO异常 e.printStackTrace(); } catch (RuntimeException e) { // 处理查询异常 e.printStackTrace(); } }
关键注意事项
- SQL Server必须配置游标模式:在你的
initDataSource方法中,JDBC URL一定要加上selectMethod=cursor,比如:
这是因为SQL Server默认会把全量结果拉到客户端内存,哪怕设置了jdbc:sqlserver://your-server:1433;databaseName=your-db;selectMethod=cursor;user=xxx;password=xxxfetchSize也无效,必须开启游标模式才能真正流式返回结果。 - 调整fetchSize值:10000是比较合理的初始值,太小会增加数据库交互次数,太大则会占用更多客户端内存,可根据你的堆内存大小调整。
- 资源释放:一定要用
try-with-resources管理JsonLOutputWriter,确保文件流被正确关闭,DbUtils的QueryRunner会自动关闭ResultSet和Statement,但要确保你的DataSource配置了正确的连接池,避免连接泄漏。 - 避免使用MapListHandler:这个Handler的设计目标是小数据集快速转换,完全不适合超大结果集,必须替换成流式处理逻辑。
这样修改后,你的程序内存占用会始终保持在fetchSize对应的行数左右,不会再出现堆内存溢出的问题,哪怕处理10GB的原始数据也能稳定运行。
内容的提问来源于stack exchange,提问作者Chet
相关产品推荐
相关产品推荐

