在单台机器上实现TB级的数据对比
前几天我在做公司项目的时候,需要对数据进行比较。数据量不大,也就几千条而已。当时功能完成后,我开始思考一个问题。就是当对比的文件是特别大的时候,我还能实现吗?那现在对问题做出假设:
问题:
现在有两个超大文件集,原始数据集和目标数据集,每个文件集中都有超过1亿的记录,数据中存在重复的记录,编写一个程序,可以找出两个数据集之间的差异,求解出三类数据:
原始数据集中存在,目标数据集中不存在,为待删除的数据记录。
原始数据集中存在,目标数据集中也存在,但内容不一致,为待修改的记录。
原始数据集不存在,目标数据集中存在,为待添加的记录。
还有一个限制条件,内存限制8G。
求解思路:
普通求解
先说我在小数据量的情况下,我使用的方法吧,
-
先将原始数加载到内存中,使用一个map记录,以数据主键为key,以数据为值.当数据存在时,后面的数据覆盖前面的数据。
-
目标数据集也装载到内存中,同样使用一个map记录.
-
遍历原始数据,以原始数据的key到目标数据的map中去获取,如果获取不到,说明当前数据仅在原始数据中存在,在目标数据中不存在,添加到待删除的记录中,如果可以获取到,检查数据与目标数据是否一致,如果一致,说明原始数据与目标数据完成一样,就跳过当前数据,如果数据不一致,说明目标数据做出来修改,则记录到待修改记录的原始数据中。
-
遍历目标数据,以目标数据的key到原始数据的map中去获取,如果获取不到,说明当前数据仅在目标数据中存在,在原始数据中不存在,添加到待添加的记录中,如果可以获取到,需要再检查数据与原始数据是否一致,如果一致,说明原始数据与目标数据完全一样,跳过当前数据;如果数据不一致,说明目标数据做出了修改,则记录到待修改记录的目标数据中。
经过这4个步骤后,数据之间的差异就被对比出来了。
错误方案:布隆过滤器
那切回到大数据集的对比吧,用这种方案肯定是无法实现的。那要怎么解呢?
还是参照普通求解的一个方案。只不过采用了布隆过滤器来替代map.但没有想到却是一条歪路。
由于布隆过滤器与map很相似,且空间占用更优,我就优先想到这个方案,具体是这样子的:
-
构建两个布隆过滤器组。原始布隆过滤器组和目标过滤器组,每个布隆器组中存在多个布隆过滤器,通过hash函数将数据均匀分布到这一组的布隆过滤器中,每个布隆过滤器控制空间占用不超过50%。
-
遍历原始数据,将原始数据的key和数据,分别填充到原始布隆过滤器组中。
-
遍历目标数据,将目标数据的key和数据,分别填充到目标布隆过滤器组中。
-
遍历原始数据,检查原始数据的key是否在目标布隆过滤器组中,如果不存在,说明数据在原始数据中存在,在目标数据中不存在,则记录到待删除的文件中;如果原始数据的key存在于目标布隆过滤器组中,检查原始数据的是否存在于目标布隆过滤器组中,如果存在,说明原始数据与目标数据一致,跳过当前数据,如果原始数据与目标数据不一致,说明数据不存在,记录到待修改前的文件中。
-
遍历目标数据,检查目标数据的key是否在原始布隆过滤器组中,如果不存在,说明数据在原始数据中不存在,在目标数据中存在,则记录到待添加的文件中;如果目标数据的key存在于目标布隆过滤器组中,检查目标数据是否存在于原始布隆过滤器组中,如果存在,说明原始数据与目标数据一致,跳过当前的数据,如果原始数据与目标数据不一致,说明数据不存在,记录到修改后的文件中。
-
经过上面的步骤后,各种数据就被筛选出来,包括添加的数据文件,修改前的数据文件,修改后数据文件,删除的数据文件,接下来就是将修改前与修改后的文件合并了。由于这里都是待修改的数据,两边数据key都会存在,但这里存在一个问题,那就是修改前的数据文件与修改后的数据文件并非有序的,无法直接输出。那接下来就是先修改前与修改后的文件有序化的过程了。
-
这里我使用的是归并排序方案。先将修改之前的文件切分,按32M为大小为限制,并且保证数据都是完整的行,切分出一个个文件,并不保证每个文件都是32M大小,只是最接近于32M的大小.
-
待修改前文件切分完成后,对每个小文件进行排序操作。使用原生java的插入排序方式,虽然时时间复杂度为O(n的平方)但排序的稳定,不会改变相同数据的前后顺序。至到所有小文件排序完成。
-
修改前的的所有小文件排序完成后,开始对修改后的文件进行切分了,同样使用原始文件切分的方式,以32M为大小限制,并我看也是数据都是完整的行。切分完一个个文件。
-
修改后的文件切会完成后,开始对切分文件排序,同样使用插入排序。至到所有小文件排序完成。
-
终于来到最后一个步骤了,那就是对修改前与修改后的文件进行合并了。按修改前–>修改后这样的对应方式。将数据标识出来。
看似一个完美的方案。当我在测试1亿的数据后,发现一个致命问题。那就是无法保证修改前与修改后的数据都会存在。最后通过对数据检查。发现布隆过滤器中还是会存在误判的问题。看来还是没有对布隆过滤器深入到位,天真的以为只要空间占用低点,就不会出现误判。现实就这么打脸。还是按在地上摩擦的那种。
正确方案:归并排序对比
不过这个错误的做法也不是一无是处。却为我开劈了一个思路。那就是既然归并排序能将两边数据都排序,那排过序的文件,能否对比出差异呢?带着这个问题,我开始思考这个问题能不能解呢?即问题变成了,对两个排序相同的数据,找出差异?还是分情况还做下分析吧:
-
原始数据1的主键小于目标数据1的主键。这种情况说明数据在原始数据中存在,在目标数据中不存在,即为待删除的数据,加入待删除的文件中。
-
原始数据1的主键等于目标数据1的主键。这种情况说明数据在原始数据中存在,在目标数据中也存在,可能为待修改的数据,也可能前后数据都没有修改,什么操作都不用。那接下来就是检查数据内容是否一致了,通过对比,数据可能一致,则直接跳过当前数据,数据可能不一致。将数据加入到待修改的文件中。
-
原始数据1的主键大于目标数据1的主键。这种情况说明数据在原始数据中不存在,在目标数据中存在,即为待添加的数据,加入待添加的文件中。
通过上面这三情况一分析。排序后的数据是可以直接对比出差异的。好像不对,我是不是漏了什么东西?原始数据1和原始数据1最小的的在个文件呢?一定在第一切分的文件中吗?由于数据是按大小切分的,排序之后的最小在哪个文件是不确定的。那要先找到第一个最小的那个数据?这如何找到呢?可以这样分析这个问题,针对所有已经 排序的小文件来说,最小的数据在哪呢?这个肯定在某个文件的第一行。只要读取所有文件的第一行数据,然后遍历对比,这样就可以找到最小的数据了,其实这就是归并排序时的合并逻辑。
但还存在一个问题,那数据的key可能会是重复的。如果我们对重复的数据加以规定,当数据的主键的key相同时,取最后操作的记录。那上面的对比流程还能正常工作吗?或者如何改进呢?由于原始数据与目标数据中的重复问题是一样的,只讨论原始数据中带有重复数据的问题如何解决了!
重复存在着两种情况:
- 单文件内的有序数据重复。
- 多文件间的数据重复。
单文件内的有序数据重复问题:
由于单文件内数据重复仅针对主键相同的数据,即当key相同时,顺序遍历查找最大值即可。
多文件间的数据重复问题:
这个问题就需要优先完成单文件内有序重复数据查找最大值。将最大值记录在读取数组中。再遍历这个数组,当主键key相同时,再查找最大值。
还是拿一个具体一示例来说明吧。
来解释下吧,从文件中读取第一行的数据“5, 481086186500,1,2”,保存到临时变量中,再从文件中读取一行数据“5, 481086186510,1,2”与临时变量中的数据做对比,key一致,则保留时间最新的数据"5, 481086186510,1,2"。将最新的数据到临时变量中。
再就来多组数据的合并吧
我来解释下吧,首先分别从文件1和文件2读取首行数据至临时最小变量中,就是图中的“5, 481086186500,1,2”和“5, 481086186520,1,2”,再对比临时变量key相同的数据,将时间最近的数据保留。输出完毕后,再分别从文件和和文件2读取数据填充至最小临时变量中。一直轮训这个过程就可以将数据输出了。
来整体看下这个数据对比的流程:
总结:
当我将整个数据对比完成后,才发现原来这就是一个稍加改造的归并排序。在归并排序的合并数据时将逻辑稍加改造 ,按数据的差异性的结果就比出来了。特别需要注意重复数据合并。按照数据的一个差异性将数据输出到添加数据的文件、修改数据文件、删除数据文件。这样一个超大型的文件对比就完成了。
如果想直接查看代码,欢迎来我的github查看,
程序的入口:
https://github.com/kkzfl22/datastruct/blob/master/src/main/java/com/liujun/datastruct/datacompare/bigfilecompare/BigFileCompareMain.java
由于整个过程中涉及代码众多,接下来将分为多章节来讲解对比的实现。敬请期待。