一种利用多线程和分块快速读取文件的方法
背景
在工作中经常会有接收文件并且读取落库的需求,读取方式都是串行读取,即一行行的读取,如果文件小还可以,但是如果文件比较大,类似于全量文件的话,这样的读取就会非常效率低。
本文主要介绍的是如何正确的将文件分块,多线程的实现方式有多种,这里用的是CompletableFuture
方法
因为我们文件里面的每条数据之间没有任何依赖关系也不存在顺序要求。如何提高读取速度,第一个想到当然就是并行读取文件,并行读取的前提就是要给文件分块,让每个线程只读取对应分块的数据,先看看我们的文件格式
可以看见我们的文件格式每一行的长度不一,同时文件也无法像TCP通过指定数据体的长度来读取数据,所以如何能正确的分块是整个方法的关键,如果分多或者分少了就会导致数据读取错误的可能。
可以看见错误的分块就会导致我们读取的数据会被截取掉一部分,截取掉多少都是随机的。这里我们用的方法是用填充来让每个分块都是正确的。具体来说就是我们在分块的时候,判断一下当前的分块位置会不会导致数据被截取,因为我们的数据是一行行的,所以最好的分块位置都是分在了行尾。如果说当前的分块位置是在一行的中间的话,那我们就要移动这个分块的位置到这行的行尾去
java
private static int THREAD_NUM = 5;
long total = file.length();
long chunkSize = total < THREAD_NUM ? total : total / THREAD_NUM;
先确定要用几个线程并行读取,然后根据线程数和文件的大小来确定每一块的大小,接下来就进行判断是否需要填充
java
private static long padding(long start, long chunkSize, File file) {
try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r")){
randomAccessFile.seek(start + chunkSize);
boolean eol = false;
//判断当前的位置是否需要填充,如果当前没有数据或者是行尾,则不需要填充
switch (randomAccessFile.read()) {
case -1:
case '\n':
eol = true;
break;
case '\r':
eol = true;
break;
default:
break;
}
//如果符合填充条件,对其进行填充,首先是读取一行数据,然后计算出这行数据的长度,然后将这行数据的长度加上前面读取了一字节,然后将这些长度加到chunkSize上
if (!eol){
String readLine = randomAccessFile.readLine();
chunkSize += readLine.getBytes().length;
//加上前面读取的一字节
chunkSize += 1;
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return chunkSize;
}
如何填充的可以看看代码注释,是参照了readline的实现思路。 经过填充后就可以确保每块的分块的最后一个位置都是在行尾。将每个分块的起始位置和分块大小储存起来再结合上CompletableFuture就可以多线程分块读取文件了
Java
Map<Long, Long> chunkMap = new HashMap<>();
for (int i = 0; i < THREAD_NUM; i++) {
chunkSize = padding(start, chunkSize, file);
chunkMap.put(start, chunkSize);
start += chunkSize;
}
CompletableFuture.allOf(chunkMap.entrySet().stream().map(entry -> CompletableFuture.runAsync( () -> handlerReportTreeBaseData(entry.getKey(), entry.getValue()))).toArray(CompletableFuture[]::new))
.exceptionally(throwable -> {
System.out.println(throwable.getMessage());
return null;
}).join();
完整实现
接下来给出整个的实现代码,欢迎大家看看有没有什么我没有考虑到的,有可能的隐藏BUG和还能优化改善的地方,欢迎讨论
Java
public class SpiltFIle {
private static int THREAD_NUM = 5;
private static void splitChunks() {
File file = new File("test.txt");
long total = file.length();
long chunkSize = total < THREAD_NUM ? total : total / THREAD_NUM;
long start = 0;
Map<Long, Long> chunkMap = new HashMap<>();
for (int i = 0; i < THREAD_NUM; i++) {
chunkSize = padding(start, chunkSize, file);
handlerReportTreeBaseData(start, chunkSize);
chunkMap.put(start, chunkSize);
start += chunkSize;
}
CompletableFuture.allOf(chunkMap.entrySet().stream().map(entry -> CompletableFuture.runAsync( () -> handlerReportTreeBaseData(entry.getKey(), entry.getValue()))).toArray(CompletableFuture[]::new))
.exceptionally(throwable -> {
System.out.println(throwable.getMessage());
return null;
}).join();
}
private static long padding(long start, long chunkSize, File file) {
try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r")){
randomAccessFile.seek(start + chunkSize);
boolean eol = false;
//判断当前的位置是否需要填充,如果当前没有数据或者是行尾,则不需要填充
switch (randomAccessFile.read()) {
case -1:
case '\n':
eol = true;
break;
case '\r':
eol = true;
break;
default:
break;
}
//如果符合填充条件,对其进行填充,首先是读取一行数据,然后计算出这行数据的长度,然后将这行数据的长度加上前面读取了一字节,然后将这些长度加到chunkSize上
if (!eol){
String readLine = randomAccessFile.readLine();
chunkSize += readLine.getBytes().length;
chunkSize += 1; //加上前面读取的一字节
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return chunkSize;
}
private static void handlerReportTreeBaseData(long start, long chunkSize) {
try (RandomAccessFile randomAccessFile = new RandomAccessFile("test.txt", "r")) {
randomAccessFile.seek(start);
long currentCount = 0L;
String line;
while (currentCount < chunkSize && (line = randomAccessFile.readLine()) != null){
if (!line.isEmpty()){
currentCount += line.getBytes().length + System.lineSeparator().getBytes().length;
System.out.println(line);
}
}
}catch (Exception ignored){
}
}
public static void main(String[] args) throws IOException {
SpiltFIle.splitChunks();
}
}
最后也是能正常的读取完文件