最近工作上有一个小需求:总数据量大概七亿左右,已经入库6.4亿左右,还有6千万左右数据失败了,这里的失败的原因可能有多种,现在需要判断这六千万的数据是导库工具处理失败的还是这些是脏数据(脏数据不会入库),而这些数据有一个唯一的id可以标识:9位的字符串(例如:103355296),因此,我需要做的就是重这七亿数据中找出这6千万的数据,换句话说,我需要把七亿的数据与正常入库的6.4亿数据进行一个比对,找出在七亿这个数据源里,而没有在6.4亿这个数据源里的数据。本质是一个去重的概念。
要解决这个问题,首先我想到用spark去解决,spark这个运算框架以及它支持的算子很容易就能完成这个数据量的比对。但是苦于没有环境,所以放弃了这个想法。那就写个java程序去做比对吧。
我们先阐述一下这个问题的难点,以便于我们针对难点去解决:
难点一:这种上亿的数据量,是没法直接放进内存的,因为装不下,如果每次以内存能容下数据量从磁盘里面读出这些id,在逐个id比对,会导致严重的IO,性能并不高。因此要是能想一种方式将这些id一次装进内存,再比对的话,那效率就很高了。
难点二:针对每个ID进行比较,如果进行字符串比较,Java的equals是会循环比较两个char数组,逐个比较字符是否一致,一致才为true,是否有更高效率的比较方式呢,例如位运算。
针对这两个难点,我首先想到的是hash,但是很快否定了,因为这个hash函数不好定义,并不能保证这个这七亿个id经过hash还是唯一,因为不同的id经过hash之后可能会在同一个桶中,要做这种精确去重,这个方式是行不通的。很快我有想到了第二种方式,使用布隆过滤,但是布隆过滤是存在误差的,于是我又想到另外一种算法:位图算法。
什么是位图算法,可以百度一下,有详细的介绍,这里就不阐述了,我只讲述我是怎么使用的,关于位图算法的实现有很多,不需要我们自己手动去写。首先我选择的方案就是使用谷歌开源的EWAHCompressedBitmap来做,但是经过我的测试,发现他的效率差于RoaringBitmap,因此我选择了RoaringBitmap来进行比对工具编写。
代码如下:我实际的代码有比较复杂的处理,为了不影响理解,改代码已经简化过,这里只想表达整个实现的结构,具体的代码需要自己实现,较为简单
public class FileComparisonUtil implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
RoaringBitmap roaringBitmap1=RoaringBitmap.bitmapOf();
RoaringBitmap roaringBitmap2=RoaringBitmap.bitmapOf();
// 输入源1的文件跟路径
String inputPath1="";
List<String> list1 = this.buildPath(inputPath1);
// 输入源2的文件跟路径
String inputPath2="";
List<String> list2 = this.buildPath(inputPath2);
//把所有id装入roaringBitmap1
list1.forEach(path-> this.process(path,roaringBitmap1));
//把所有id装入roaringBitmap2
list2.forEach(path-> this.process(path,roaringBitmap2));
//比较
roaringBitmap1.andNot(roaringBitmap2);
//输出文件路径
String outputPath="";
//序列化到磁盘
this.write(roaringBitmap1,outputPath);
}
private void write(RoaringBitmap roaringBitmap1, String outputPath) {
try {
List<String> list=new ArrayList<>();
Iterator<Integer> iterator = roaringBitmap1.iterator();
File file=new File(outputPath);
FileWriter fileWriter=new FileWriter(file,true);
while (iterator.hasNext()){
Integer next = iterator.next();
String s = next.toString();
fileWriter.append(s);
fileWriter.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
private List<String> buildPath(String inputPath1) {
// 获得所有文件路径
return null;
}
public List<String> read(String path){
//读取文件,建议大文件分割读取,避免内存溢出
return null;
}
public void process(String path,RoaringBitmap roaringBitmap){
List<String> read = read(path);
// 注意如果这里id是000003456这样以0开头的,roaringBitmap存的实际是3456,反序列化时需要填上00000,
read.forEach(id-> roaringBitmap.add(Integer.valueOf(id)));
}
首先是一个构造数据源的xls文件的方法,因为我们的文件被拆分了,所以需要构造拆分后的每个而文件路径;
其次是一个读取文件并写入RoaringBitmap的方法,因为我的id是一个只有一列的xls文件,而且有多个,单个200万左右,于是我就以一个xls文件为单位读取,如果你的文件比较大,建议先进行分割,避免内存放不下。这样其实我们内存里面每次只装了一个xls文件的id,并且这些读取之后立马装入RoaringBitmap,因此不会内存溢出。
然后是一个RoaringBitmap比较的操作,得到RoaringBitmap1中包含但是RoaringBitmap2中不包含的id,
最后序列化到磁盘
总结:选用这个方式,其实有一点取巧,因为这里列举的id是9位的,在整数范围内,因此不需要作过多的操作就可以写入RoaringBitmap,但实际上我真实的业务是12位的id,我代码里面对这些id进行了许多判断和切割,使其映射到了9位,而且保证唯一,因为这些复杂的切割和映射影响表达RoaringBitmap的使用,所以这里统一用了9位这样一个而整数范围内的id来作解释。在真实的情景下,我们的id长度可能远超过整数范围,还有可能并不是数字型的字符串,这些就需要我们根据具体情况去处理,如果去我们能忍受误差,可以使用布隆过滤器。