海量数据处理方案

什么是海量数据?

所谓的海量数据从字面上理解就是数据多到已经用大海来形容了,它指的就是数据量太大,无法在较短时间内迅速解决,无法一次性装入内存。

少量数据处理方案

对于在内存中可以一次性快速处理的少量数据,我们有很多方式对数据进行处理。

  • 数据排序:通过快排、归并排序、桶排序等等多种方式都可以进行处理;

  • 数据查询:通过遍历、二分查找等方式,或者通过构建 hash 表、红黑树、位图等数据结构实现快速查询;

  • 求 TOPK:

    • 对于静态数据,可以使用排序后遍历,或者使用快排的思想快速查找分区点等方式进行处理;
    • 对于动态数据,可以借助堆这种数据结构实现快速查找 TOPK;当然,静态数据也能使用堆的方式求 TOPK;
  • 两组数据去重/找重:

    • 对一组数据构建 hash 表,另一组数据基于这组 hash 表实现去重/找重;
    • 先对两组数据分别进行排序,然后使用双指针(分别指向一组数据)的方式进行逐个遍历,实现去重/找重;

对于少量数据的处理,因为当前数据量较小,可以一次性加载到内存中进行处理,所以对于这种数据的处理,我们可以找到很多种方式对数据进行处理。

海量数据处理面临的问题

我们要想对海量数据实现排序、查询、求 TOPK、去重等操作,我们没法直接把数据一次性加载到内存中,然后一次性进行处理,因为海量数据往往面临以下两个问题:

  • 单台机器内存不够;
  • 单台机器对数据的处理速度过慢。

海量数据处理的核心思想

基于海量数据处理面临的上述两个问题,我们可以很容易想到一些对于海量数据进行处理的方案:

  • 不必把数据一次性加载到内存中,而是通过分批处理的方式,把外存中的数据加载到内存中进行处理;
  • 单机内存存不下,那么可以扩展为多机,对于外存中的海量数据,把数据分片到不同的机器中,用多机内存进行处理;
  • 对于单机对数据处理速度慢的问题,可以通过多机并行计算的方式进行并行处理,提升整体的处理速度。

海量数据处理的一些常见案例及对应处理方案

排序问题

案例:给 10 GB 的订单文件进行排序,排序条件是订单的总金额。

首先需要判断,当前内存中能否一次性处理这 10 GB 的文件?如果能处理的话,直接在内存中通过排序的方式进行处理即可。

如果内存中无法一次性处理 10 GB 大小的文件,我们应该如何处理?

我们假设内存大小是 1.5 GB ,我们每次从 10 GB 的文件中读取 1 GB (预留 0.5 GB 的空间,用来申请临时变量等使用)的文件到内存中;

我们在内存中对这 1 GB 的文件排好序后,将排好序的 1 GB 文件写到外存中,然后释放内存空间;

接着继续往下处理第 2 个 1 GB、第三个 1 GB ......依次类推,最终可以得到 10 个 1 GB 的小文件,每个小文件内部都是有序的。

接下来,我们需要将这 10 个各自有序的 1 GB 小文件合并成一个大的 10 GB 的有序文件;

这里的处理思路也非常简单,我们可以从每个小文件中取最小的一个值,放入内存中 size 为 10 的数组中,找到数组中最小的值,写 10 GB 的新文件(也可以是覆盖原来的 10 GB 文件)第一个位置;

然后从内存中删除这个最小值,并在该值对应的 1 GB 有序小文件中读取第二个值放到数组中,重复之前的操作;

最终我们即可将 10 个 1 GB 的有序文件整合成 1 个 10 GB 的有序文件。

针对上面这种情况,其实我们还可以继续做一些优化:

(1)IO 操作是比较耗时的,所以我们每次不仅仅只是从外存中读取 1 个值处理,可以在读取时给每个小文件添加一层缓存,大小 100 MB ,读取时优先从缓存中获取,缓存读完后再触发下一次 IO 操作从文件中读取数据;

(2)同样的思路,在写回文件时,也可以使用缓存来减少 IO 操作,提高效率;

(3)上述问题在单机的情况下,我们顺序将 10 GB 的文件读取为 10 个 1 GB 的小文件,然后用多路归并的方式合并;在多机情况下,假设我们有 10 台机器,那么每台机器可以读取 1 GB 的文件,同时进行排序,再按照上面的思路进行后续的整合,可以进一步提高效率。

对于上述问题,我们使用了多路归并的思想进行了处理,实际情况中,如果订单金额最小最大值我们是已知的,且分布较为均匀的话,我们还可以使用如下的方式进行处理:

假设订单金额在 0-999 元的范围内,顺序读取 10 GB 文件,按照金额大小,将订单拆分到不同的桶中;

之后,按序将每个小文件读取到内存中排好序,然后写回到 10 GB 的大文件中;由于不同小文件之间金额有大小关系,所以顺序写回的10 个小文件写好后整个大文件就是有序的。

查询问题

案例:判断当前登录人 IP 地址是否在 IP 地址白名单内,已知 IP 地址白名单现有 10 亿个 IP 地址。

单机处理方式:

这个问题首先我们已知 IP 地址可以用 32 位的整数来表示,占用 4 个字节;对于 10 亿个 IP 地址,总共占用空间大约 4 GB;假设我们内存空间是 1 GB,那么是否可以在内存中直接判断登录人 IP 是否在这 10 亿个 IP 地址中呢?

首先考虑可以支持快速查找的数据结构: hash 表、红黑树,发现这种数据结构无法在 1 GB 的内存地址存储 4 GB 的数据;考虑到节省空间,可以使用位图来进行处理:

IP 地址表示为 32 位整数,int 类型数字范围最大可以在 0~2^32-1,所以存储 int 类型数字,我们总共需要 2^32 个二进制位来进行存储;8 个二进制位为1个字节,所以 2^32 / 8 个字节,可得大约需要 500 M 空间的位图来存储。

位图构建:扫描 IP 地址白名单文件,找到 IP 地址对应 int 类型数字所在的位图中的二进制位的位置,将对应二进制位置为1;

实时查询:通过当前登录人 IP,找到位图中对应的位置,判断当前位是否为 1 即可处理。

多机处理方式:

对于白名单服务,我们也可以考虑使用多机进行处理,降低单机的压力。

假设我们有 4 台机器,每台机器有 1 GB 的内存,我们需要处理这 10 亿个 IP 地址( 500 MB )。我们首先需要扫描外存中的白名单文件,对每个 IP 地址求 MD5(确保分布均匀),再对 4 求余,然后将该条白名单 IP 存储对应的机器内存中(存储形式按照内存是否富余可以考虑 hash 表、红黑树、位图等方式)。在校验当前登录人 IP 时,我们也对当前登录人 IP 求 MD5,再对 4 求余,然后找到对应的机器,再在该机器上判断是否在白名单内即可。

TOPK 问题

案例:对 100 GB 搜索关键字⽂件,统计其中出现频率最高的 TOP 100 的关键词

针对这类问题,我们首先假设内存足够大,可以直接一次性加载处理的情况,我们应该如何处理?

(1)首先统计每个关键词出现的频率,可以用如下方法

  • 方法1:先进行排序,然后顺序扫描,边扫描边统计;
  • 方法2:使用 hash 表进行频率统计

(2)然后我们可以使用堆的方式,按照频率统计出现频率 TOP 100 的关键词。

简单的代码实现如下:

ini 复制代码
public String[] top100(String[] keywords) {
  if (keywords == null) return null;
  // hash表统计频率
  Map<String, Integer> map = new HashMap<>();
  for (String keyword : keywords) {
    map.put(keyword, map.getOrDefault(keyword, 0) + 1);
  }
  // 小顶堆保留频率最大的top100
  PriorityQueue<String> heap = new PriorityQueue<>(100, (o1, o2) -> map.get(o1) - map.get(o2));
  for (Map.Entry<String, Integer> entry : map.entrySet()) {
    String keyword = entry.getKey();
    int frequency = entry.getValue();
    if (heap.size() < 100) {
      heap.offer(keyword);
    } else if (frequency > map.get(heap.peek())) {
      heap.poll();
      heap.offer(keyword);
    }
  }
  // 结果输出
  String[] res = new String[100];
  int i = 0;
  while (!heap.isEmpty()) {
    res[i++] = heap.poll();
  }
  return res;
}

实际上,100 GB 数据无法无法一次性加载到内存中,对于海量数据的频率 topK ,我们按如下方式进行处理:

(1)首先还是需要对每个关键词的频率进行统计,统计关键词频率的方式跟内存中统计一样,都可以使用排序后遍历和 hash 表统计的方式。

a. 排序后遍历的方式较为简单,首先对于海量数据排序,我们可以使用之前提到的海量数据排序问题的处理方式,得到一个有序的关键词文件;之后我们顺序扫描有序文件中的关键词到内存中,并记录同一关键字连续出现的个数,统计每个关键词的形式,之后以 key-val 的形式({"关键字":"出现次数"})存储到外存中形成一份关键词频率文件。

b. 对于 100 GB 海量数据关键词频率的统计,还可以使用 hash 表的方式:

假设机器有 15 GB 的空间,我们对每个关键词求 MD5,然后对 10 取余的方式进行数据分片,这样余数就落到 0-9 的范围内,就可以把 1 个大文件拆分为 10 个小文件,编号为 0.txt~9.txt;

我们可以先把 0.txt 加载到内存中,然后使用 hash 表来进行频率统计,统计后生成 key-val 的频率统计小文件;

之后依次读取 1.txt~9.txt,这样就生成 10 个存储 key-val 的频率统计小文件。

(2)统计完频率以后,我们需要读取频率文件,然后获取频率最高的 top100 关键词。这里依然可以使用堆的方式,读取外存中的 key-val 到内存中,然后通过容量为 100 的小顶堆获取频率最高的 top100 关键词。

去重/找重问题

案例:有两个文件 0.txt 和 1.txt,各有 50 亿条 URL,每条占内存为 64 字节,单机内存限制 4 GB,需要找出这两个文件中相同的 URL

我们首先考虑内存足够大,可以一次性将所有数据加载到内存中的情况,如何处理两个文件中找到重复 URL 的问题,有两种方式可以作为参考:

(1)方式1:先对两个文件各自进行排序,然后使用双指针的方式,a 指针指向文件 0.txt 开头,b 指针指向文件 1.txt 开头,对比指针位置是否相等,相等就将重复值取出保存;不相等的情况下,如果 a 指针的值小于 b 指针的值,那么 a 指针后移一位,否则 b 指针后移一位,可参考如下代码:

ini 复制代码
public List<Integer> findDuplicate(int[] arr1, int[] arr2) {
  if (arr1 == null || arr2 == null) return null;
  // 先对两个数组进行排序
  Arrays.sort(arr1);
  Arrays.sort(arr2);
  // 重复数字结果
  List<Integer> res = new ArrayList<>();
  // a、b双指针
  int a = 0, b = 0;
  while (a < arr1.length && b < arr2.length) {
    // 数组内部重复的值只统计一次
    if (a < arr1.length -1 && arr1[a] == arr1[a + 1]) {
      a++;
      continue;
    }
    if (b < arr2.length -1 && arr2[b] == arr2[b + 1]) {
      b++;
      continue;
    }
    // 相同的值取出
    if (arr1[a] == arr2[b]) {
      res.add(arr1[a]);
      a++;
      b++;
    } else if (arr1[a] < arr2[b]) {
      a++;
    } else {
      b++;
    }
  }
  return res;
}

(2)方式2:先对一个文件构建 hash 表,然后遍历另一个文件,通过 hash 表来找到重复的 URL 。

对于海量数据而言,仍然可以使用上面的两种方式来进行处理:

(1)方式1:排序+双指针

先对两个文件 0.txt 和 1.txt 进行排序,具体方案可以参考上文排序问题里面的案例;

然后使用 a 、 b 两个指针,分别指向 0.txt 和 1.txt 文件的第一个值,读取到内存中,如果值重复就写到新的文件 2.txt 中,不重复就按照上面内存中少量数据处理方式后移较小的指针,直到其中某个文件被读取完即可;

这里可以优化为:每次从外存中读取 100 MB 的数据进行处理,减少 IO 操作。

(2)方式2:hash 表找重

构建 hash 表的过程需要在内存中进行,一次性加载 0.txt 和 1.txt 到内存中是不可行的,通过前面案例的启发,很容易想到使用分片的方式:

扫描 0.txt 和 1.txt 对 URL 求 MD5,然后对 20 求余(这里只是举例,具体分片多少个需要根据内存的实际情况),将 0.txt 和 1.txt 中余数为 0 的 URL 放到内存中,然后通过 hash 表的方式找到重复的元素,写入新文件;

扫描 0.txt 和 1.txt 对 URL 求 MD5,然后对 20 求余(这里只是举例,具体分片多少个需要根据实际情况),将 0.txt 和 1.txt 中余数为 1 的 URL 放到内存中,然后通过 hash 表的方式找到重复的元素,写入新文件;

以此类推......

最终我们会扫描 20 次,并将所有重复的 URL 写到新文件中。

这种方式虽然可以实现找重,但是扫描了 20 次文件,所以可以使用多机的方式进行处理,我们使用 20 台机器,把余数为 0~19 的分别放到不同的机器上进行处理,这样只需要扫描 1 次,极大提高了处理效率。

总结

对于海量数据处理问题,在实际情况中,我们可以先考虑单机内存足够处理的情况下需要采用何种方式;

当我们找到单机内存充足情况的处理方案以后,再通过一些海量数据的通用处理手段,例如:外存分批读取、分片、多机并行处理等方式,最终达到成功处理海量数据的目标。

推荐阅读

工作了5年你居然不知道版本号有这些规范?

线程池 ThreadPoolExecutor 基础介绍

Netty-EventLoop实现原理

Netty内存分配

从单线程到多线程,再到虚拟线程

招贤纳士

政采云技术团队(Zero),Base 杭州,一个富有激情和技术匠心精神的成长型团队。规模 500 人左右,在日常业务开发之外,还分别在云原生、区块链、人工智能、低代码平台、中间件、大数据、物料体系、工程平台、性能体验、可视化等领域进行技术探索和实践,推动并落地了一系列的内部技术产品,持续探索技术的新边界。此外,团队还纷纷投身社区建设,目前已经是 google flutter、scikit-learn、Apache Dubbo、Apache Rocketmq、Apache Pulsar、CNCF Dapr、Apache DolphinScheduler、alibaba Seata 等众多优秀开源社区的贡献者。

如果你想改变一直被事折腾,希望开始折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊......如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的技术团队的成长过程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 zcy-tc@cai-inc.com

微信公众号

文章同步发布,政采云技术团队公众号,欢迎关注

相关推荐
xiaoshiguang34 小时前
LeetCode:222.完全二叉树节点的数量
算法·leetcode
爱吃西瓜的小菜鸡4 小时前
【C语言】判断回文
c语言·学习·算法
别NULL4 小时前
机试题——疯长的草
数据结构·c++·算法
TT哇4 小时前
*【每日一题 提高题】[蓝桥杯 2022 国 A] 选素数
java·算法·蓝桥杯
yuanbenshidiaos6 小时前
C++----------函数的调用机制
java·c++·算法
唐叔在学习6 小时前
【唐叔学算法】第21天:超越比较-计数排序、桶排序与基数排序的Java实践及性能剖析
数据结构·算法·排序算法
ALISHENGYA6 小时前
全国青少年信息学奥林匹克竞赛(信奥赛)备考实战之分支结构(switch语句)
数据结构·算法
chengooooooo6 小时前
代码随想录训练营第二十七天| 贪心理论基础 455.分发饼干 376. 摆动序列 53. 最大子序和
算法·leetcode·职场和发展
jackiendsc6 小时前
Java的垃圾回收机制介绍、工作原理、算法及分析调优
java·开发语言·算法
游是水里的游7 小时前
【算法day20】回溯:子集与全排列问题
算法