Big mobile md5 file filter and shuffle
Status: Not started
我们现在有两个文件,文件 A 有 15 亿条数据,每条数据是一个手机号的 md5 值。文件 B 有 6 亿条数据,同样每条数据是一个手机号 md5 值。现在想要将文件 A 中与文件 B 重复的手机号去除。文件 A 和文件 B 都不能一次性加载入内存。
方案一
最容易想到的,也更普遍的,就是将文件 A 分成 20份(比如叫 A1,A2,A3…..),文件 B 分成 20 份(B1,B2,B3……),然后将 A1,B1 读入内存,然后使用 Set 等取差集。将 A1,B2 读入内存,做差集。很像笛卡尔积的样子。
方案二
我们不将 A 文件切分,将文件 B 切分(比如叫文件 B1,B2,B3….),然后将文件 B1 读入内存,流式读取文件 A,判断文件 A 的每一行是否在 B1 内,如果在说明重复,如果不在说明可能不重复,将可能不重复的写入到文件 A1。
将文件 B2 加载入内存,流式读取文件 A2,将 A2 中与 B2 重复的过滤掉,得到 A3
将文件 B3 加载入内存,流式读取文件 A3,将 A3 中与 B3 重复的过滤掉,得到 A4
以此类推。。。
我们应该尽可能将文件 B 不要切分数量太多,这样就能少遍历几次 A。
方案三
由于文件中都是手机号的 md5,我们可以按照 md5 的前两位字母做分片,这样会将文件 A 和文件 B 各分为 256 个文件。
类似的文件名为:00.csv, 0a.csv, 0b.csv, 0c.csv, 0d.csv ….
这样做过滤时,就是拿文件 A 的 00.csv 和文件B 的 00.csv,将他们加载到内存,然后做过滤。得到一个过滤后的文件 00_filtered.csv
类似的,拿文件A 的 0a.csv 和文件 B 的 0a.csv,将他们呢加载到内存,然后做过滤。得到一个过滤后的文件 0a_filtered.csv
以此类推。我们会拿到所有的过滤文件,最后将这过滤后的 256 个文件做合并就好了。
关于方案三,我有示例代码:
/**
* 方案设计:
* <p>
* 第一阶段:对第一个文件进行分片
* <p>
* 读取第一个文件的MD5值
* 使用前N位(如前2位)作为分片依据
* 将MD5值分散写入不同的小文件中(称为A组文件)
* 第二阶段:处理第二个文件并标记重复项
* <p>
* 读取第二个文件的每个MD5值
* 使用前N位(如前2位)作为分片依据
* 将MD5值分散写入不同的小文件中(称为B组文件)
* 第三阶段:B 组文件中过滤 A 组文件中重复的MD5值
* <p>
* 对应处理A组和B组的同编号文件
* 将A组文件的MD5值加载到HashSet中
* 检查B组文件中的每个MD5值是否在HashSet中存在
* 如果存在说明重复,不做任何操作。
* 如果不存在说明不重复,将该MD5值写入最终结果文件
*
* @param areaFilePath 要过滤的地区的 md5 数据
* @param packageFile 底包文件
* @param resultFile 底包过滤出重复的最终结果文件
*/
private void bigFileFilter(String areaFilePath, String packageFile, String resultFile) {
AtomicLong resultFileRowCount = new AtomicLong(0L);
String areaFileDir = FileUtil.getParent(areaFilePath, 1);
String packageFileDir = FileUtil.getParent(packageFile, 1);
// 第一阶段:处理第一个文件
fileSplit(areaFilePath, areaFileDir);
// 第二阶段:处理第二个文件
fileSplit(packageFile, packageFileDir);
// 第三阶段:B 组文件中过滤 A 组文件中重复的MD5值
// 对应处理A组和B组的同编号文件
// 将A组文件的MD5值加载到HashSet中
// 检查B组文件中的每个MD5值是否在HashSet中存在
// 如果存在说明重复,不做任何操作。
// 如果不存在说明不重复,将该MD5值写入最终结果文件
FileAppender resultFileAppender = new FileAppender(FileUtil.file(resultFile), 1000_0000, true);
FileUtil.loopFiles(areaFileDir + File.separator + "分组").forEach(file -> {
AtomicLong filterCount = new AtomicLong(0L);
String key = file.getName().substring(0, 2);
FileReader areaFileReader = new FileReader(file);
HashSet<String> areaMd5Set = new HashSet<>();
areaFileReader.readLines((LineHandler) s -> areaMd5Set.add(s));
File Bfile = FileUtil.file(packageFileDir + File.separator + "分组" + File.separator + key + ".csv");
if (!Bfile.exists()) {
log.error("文件不存在:{}", packageFileDir + File.separator + "分组" + File.separator + key + ".csv");
return;
}
FileReader packageFileReader = new FileReader(Bfile);
packageFileReader.readLines((LineHandler) s -> {
if (!areaMd5Set.contains(s)) {
resultFileAppender.append(s);
resultFileRowCount.incrementAndGet();
} else {
filterCount.incrementAndGet();
}
});
resultFileAppender.flush();
log.info("file: {} 过滤掉的条数:{}", file.getName(), filterCount.get());
});
log.info("resultFile生成总条数:{}", resultFileRowCount.get());
}
private static void fileSplit(String areaFilePath, String areaFileDir) {
// 第一阶段:对第一个文件进行分片
// 读取第一个文件的MD5值
// 使用前N位(如前2位)作为分片依据
// 将MD5值分散写入不同的小文件中(称为A组文件)
FileReader areaFileReader = new FileReader(FileUtil.file(areaFilePath));
Map<String, FileAppender> fileAppenderMap = new HashMap<>();
areaFileReader.readLines((LineHandler) s -> {
// 如果不是md5值的,跳过
if (s.length() != 32) {
return;
}
String key = s.substring(0, 2);
FileAppender fileAppender = fileAppenderMap.get(key);
if (fileAppender == null) {
fileAppender = new FileAppender(FileUtil.file(areaFileDir + File.separator + "分组" + File.separator + key + ".csv"), 20_0000, true);
fileAppenderMap.put(key, fileAppender);
}
fileAppender.append(s);
});
for (FileAppender appender : fileAppenderMap.values()) {
appender.flush();
}
}
```# Big mobile md5 file filter and shuffle