在 Java(Spring 引导)应用程序中读取 CSV 文件的性能问题

Peformance issues reading CSV files in a Java (Spring Boot) application

我目前正在开发基于 spring 的 API,它必须转换 csv 数据并将它们公开为 json。 它必须读取包含 500 多列和 250 万行的大型 CSV 文件。 我不能保证文件之间有相同的 header(每个文件可以有一个完全不同的 header),所以我无法创建一个专用的 class 来提供映射使用 CSV headers。 当前 api 控制器正在调用一个 csv 服务,该服务使用 BufferReader 读取 CSV 数据。

该代码在我的本地计算机上运行良好,但速度非常慢:处理 450 列和 40 000 行大约需要 20 秒。 为了提高处理速度,我尝试用 Callable(s) 实现多线程,但我不熟悉这种概念,所以实现可能是错误的。

除此之外 api 在服务器上 运行 时 运行 堆内存不足,我知道一个解决方案是增加可用内存量,但我怀疑在 Callable(s) 中对字符串进行的 replace() 和 split() 操作会消耗大量堆内存。

所以我其实有几个问题:

#1。我怎样才能提高 CSV 读取的速度?

#2。 Callable 的多线程实现是否正确?

#3。我怎样才能减少进程中使用的堆内存量?

#4。您知道在逗号处拆分并替换每个 CSV 行中的双引号的不同方法吗? StringBuilder 在这里会有帮助吗? StringTokenizer 呢?

下面是CSV方法

  public static final int NUMBER_OF_THREADS = 10;

   public static List<List<String>> readCsv(InputStream inputStream) {
            List<List<String>> rowList = new ArrayList<>();
            ExecutorService pool = Executors.newFixedThreadPool(NUMBER_OF_THREADS);
            List<Future<List<String>>> listOfFutures = new ArrayList<>();
            try {
                    BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
                    String line = null;
                    while ((line = reader.readLine()) != null) {
                            CallableLineReader callableLineReader = new CallableLineReader(line);
                            Future<List<String>> futureCounterResult = pool.submit(callableLineReader);
                            listOfFutures.add(futureCounterResult);
                    }
                    reader.close();
                    pool.shutdown();
            } catch (Exception e) {
                    //log Error reading csv file
            }

            for (Future<List<String>> future : listOfFutures) {
                    try {
                            List<String> row = future.get();
                    }
                    catch ( ExecutionException | InterruptedException e) {
                            //log Error CSV processing interrupted during execution
                    }
            }

            return rowList;
    }

和 Callable 实现

public class CallableLineReader implements Callable<List<String>>  {

        private final String line;

        public CallableLineReader(String line) {
                this.line = line;
        }

        @Override
        public List<String> call() throws Exception {
                return Arrays.asList(line.replace("\"", "").split(","));
        }
}

我认为将这项工作拆分到多个线程上不会带来很大的改进,而且实际上可能会消耗更多内存,从而使问题变得更糟。主要问题是使用过多的堆内存,性能问题很可能是由于剩余可用堆非常小时垃圾收集过多(但最好进行测量和分析以确定性能问题的确切原因)。

replacesplit 操作的内存消耗会更少,而且在这种方法中需要将文件的全部内容读入内存这一事实会消耗更多。每行可能不会占用太多内存,但乘以数百万行,它就会全部加起来。

如果您的计算机上有足够的可用内存来分配足够大的堆来容纳全部内容,那将是最简单的解决方案,因为它不需要更改代码。

否则,在有限的内存中处理大量数据的最佳方法是使用 streaming 方法。这意味着文件的每一行都会被处理,然后直接传递给输出,而不会收集其间内存中的所有行。这将需要更改方法签名以使用 return 类型而不是 List。假设您使用的是 Java 8 或更高版本,Stream API 可能会很有帮助。您可以这样重写该方法:

public static Stream<List<String>> readCsv(InputStream inputStream) {
    BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
    return reader.lines().map(line -> Arrays.asList(line.replace("\"", "").split(",")));
}

请注意,如果出现 I/O 错误,这会引发 未检查 异常。

这将由方法的调用者根据需要读取和转换输入的每一行,并且如果不再引用以前的行,将允许对它们进行垃圾回收。这就要求这个方法的调用者也逐行消费数据,这在生成 JSON 时可能会很棘手。 JakartaEE JsonGenerator API 提供了一种可能的方法。如果您需要这部分的帮助,请打开一个新问题,包括您当前如何生成的详细信息 JSON。

尝试使用 Spring 批处理,看看它是否对您的方案有帮助。

参考:https://howtodoinjava.com/spring-batch/flatfileitemreader-read-csv-example/

与其尝试不同的方法,不如先尝试 运行 使用分析器,看看时间实际花在了哪里。并使用此信息来更改方法。

Async-profiler 是一个非常可靠的分析器(而且是免费的!),它会给你一个很好的印象,让你知道时间都花在了哪里。它还会显示垃圾收集所花费的时间。所以你可以很容易地看到垃圾回收造成的 CPU 利用率。它还能够进行分配分析以确定正在创建哪些对象(以及在哪里)。

有关教程,请参阅以下内容 link