You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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,比如:
    jdbc:sqlserver://your-server:1433;databaseName=your-db;selectMethod=cursor;user=xxx;password=xxx
    
    这是因为SQL Server默认会把全量结果拉到客户端内存,哪怕设置了fetchSize也无效,必须开启游标模式才能真正流式返回结果。
  • 调整fetchSize值:10000是比较合理的初始值,太小会增加数据库交互次数,太大则会占用更多客户端内存,可根据你的堆内存大小调整。
  • 资源释放:一定要用try-with-resources管理JsonLOutputWriter,确保文件流被正确关闭,DbUtils的QueryRunner会自动关闭ResultSet和Statement,但要确保你的DataSource配置了正确的连接池,避免连接泄漏。
  • 避免使用MapListHandler:这个Handler的设计目标是小数据集快速转换,完全不适合超大结果集,必须替换成流式处理逻辑。

这样修改后,你的程序内存占用会始终保持在fetchSize对应的行数左右,不会再出现堆内存溢出的问题,哪怕处理10GB的原始数据也能稳定运行。

内容的提问来源于stack exchange,提问作者Chet

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.11 08:47:37