如何快速加载2000万条long型数据至Record类?
优化2000万条Long类型CSV数据加载速度的方案
嘿,我看到你在处理2000万条long类型数据的CSV加载时遇到了棘手的速度问题——每秒仅1000条的效率确实太低了,这完全没发挥出Java处理大数据的潜力。咱们来拆解下问题根源,再给出针对性的优化方案:
先分析可能的性能瓶颈
从你给出的代码片段(推测是用BufferedReader逐行读取+逐行解析+逐条插入Sorting<Record>)来看,慢的原因大概率集中在这几点:
- 逐行解析字符串转long的开销太大,频繁创建String和Record对象触发GC
- 如果
Sorting<Record>的add操作是边插入边维护有序性(比如每次插入都要找位置移动元素),那2000万条数据的时间复杂度会是O(n²),这绝对是性能灾难 - 原生的
BufferedReader+手动解析的效率远不如专门优化过的CSV解析库
具体优化方案
1. 替换为高性能CSV解析库
放弃手动逐行解析,用专门优化过的库来处理CSV,比如Univocity Parsers(它的性能比OpenCSV快好几倍,尤其适合大数据量)。这类库会批量读取字节、减少字符串对象创建,大幅提升解析速度。
示例代码:
private static void loadArray(String filepath, Sorting<Record> orderedArray) throws IOException, SortingException { System.out.println("\nLoading data from file...\n"); // 初始化Univocity的CSV解析器,配置为单列、跳过空行 CsvParserSettings settings = new CsvParserSettings(); settings.setHeaderExtractionEnabled(false); settings.setSkipEmptyLines(true); settings.setLineSeparatorDetectionEnabled(true); CsvParser parser = new CsvParser(settings); // 批量读取,每次读10000条 parser.beginParsing(new FileReader(filepath)); String[] row; List<Record> batch = new ArrayList<>(10000); while ((row = parser.parseNext()) != null) { long value = Long.parseLong(row[0].trim()); batch.add(new Record(value)); // 批量插入到有序数组 if (batch.size() >= 10000) { orderedArray.addAll(batch); // 确保Sorting有批量add的方法,如果没有就自己实现 batch.clear(); } } // 处理剩余的最后一批数据 if (!batch.isEmpty()) { orderedArray.addAll(batch); } parser.stopParsing(); }
2. 彻底重构"边读边排序"的逻辑(最关键的优化)
如果你的Sorting<Record>是在每次add时都维护数组的有序性(比如插入排序逻辑),那2000万条数据的插入操作会慢到离谱。正确的做法是先把所有数据读到一个普通数组里,一次性排序后再放入Sorting容器:
private static void loadArray(String filepath, Sorting<Record> orderedArray) throws IOException, SortingException { System.out.println("\nLoading data from file...\n"); // 第一步:批量读取所有数据到普通数组 List<Long> allValues = new ArrayList<>(20000000); // 预初始化容量,避免扩容开销 CsvParserSettings settings = new CsvParserSettings(); settings.setHeaderExtractionEnabled(false); settings.setSkipEmptyLines(true); CsvParser parser = new CsvParser(settings); parser.beginParsing(new FileReader(filepath)); String[] row; while ((row = parser.parseNext()) != null) { allValues.add(Long.parseLong(row[0].trim())); } parser.stopParsing(); // 第二步:一次性排序 Collections.sort(allValues); // 第三步:把排序后的数据批量放入Sorting容器 for (long val : allValues) { orderedArray.add(new Record(val)); // 或者如果有批量add方法,直接传入转换后的Record列表 } }
这样时间复杂度从O(n²)降到O(n log n),速度会有质的飞跃。
3. 减少对象创建开销(进阶优化)
如果Record类只是包装一个long,那频繁创建Record对象会带来GC压力。可以考虑:
- 让
Sorting直接支持long类型,避免包装成Record对象 - 或者使用对象池复用Record对象(不过对于2000万条来说,对象池可能意义不大,不如直接用基本类型数组)
4. 直接读取字节解析,跳过字符串转换(极致优化)
如果还想进一步提升速度,可以直接用FileChannel读取文件字节,手动解析字节流中的数字,完全避免String对象的创建:
private static void loadArray(String filepath, Sorting<Record> orderedArray) throws IOException, SortingException { System.out.println("\nLoading data from file...\n"); try (FileChannel channel = FileChannel.open(Paths.get(filepath), StandardOpenOption.READ)) { ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024); // 1MB缓冲区 StringBuilder sb = new StringBuilder(); while (channel.read(buffer) != -1) { buffer.flip(); while (buffer.hasRemaining()) { byte b = buffer.get(); if (b == '\n' || b == '\r') { if (sb.length() > 0) { long val = Long.parseLong(sb.toString()); orderedArray.add(new Record(val)); sb.setLength(0); } } else { sb.append((char) b); } } buffer.clear(); } // 处理最后一行 if (sb.length() > 0) { long val = Long.parseLong(sb.toString()); orderedArray.add(new Record(val)); } } }
这种方式减少了大量String对象的创建,GC压力会小很多,速度也会更快。
最后,一定要先检查Sorting<Record>的add方法实现,如果是边插入边排序,那必须改成先批量读取再整体排序,这是提升速度的核心。
内容的提问来源于stack exchange,提问作者user9039337




