一、基础知识
1.1 哈希函数
- 哈希函数又称散列函数,其输入域可以是非常大的范围,但是输出域是固定范围
- 性质
- 典型的哈希函数都拥有无限的输入值域
- 输入值相同时,返回值一样
- 输入值不同时,返回值可能一样,也可能不一样
- 哈希函数优劣的关键
- 不同输入值得到的哈希值,整体均匀地分布在输出域上
1.2 Map-Reduce
- Map-Reduce分为两个阶段
- Map阶段
- 通过哈希函数把大任务分成多个子任务
- 同样哈希值的子任务会被分配到同一个计算节点
- Reduce阶段
- 子任务并发处理,然后合并结果
- Map阶段
- Map-Reduce难点不在于理论,而在于工程,需要考虑以下问题
- 备份的考虑,分布式存储的设计细节,以及容灾策略
- 任务分配策略与任务进度跟踪的细节设计,节点状态的呈现
- 多用户权限的控制
- 应用实例,用Map-Reduce统计一篇文章中每个单词出现的个数
- 预处理,去掉标点符号、处理英文连字符等,得到只包含单词的文本
- Map阶段, 对每个单词生成词频为1的记录,例如(man, 1)。此时如果一个单词出现了多次,则有多个词频为1的记录。通过哈希函数得到每个单词的哈希值,根据哈希值把统计任务分成不同的子任务。相同的单词哈希值相同,会被分到相同的子任务中。
- Reduce阶段,在每个子任务中,合并相同单词的词频统计。最后把所有子任务中的词频统计合并,即得到结果。
1.3 外排序
- 外排序与内排序是相对应的两个概念
- 内排序指排序过程中所有数据都放在内存中的排序算法,我们熟悉的冒泡排序、选择排序、插入排序、快速排序、堆排序、归并排序等都是内排序
- 外排序指排序过程中只有部分数据会放在内存中的排序算法,一般应用于内存不足或数据量过大的场景
- 外排序由两个不同的阶段组成
- 第一阶段,采用适当地内部排序方法对输入文件的每个片段进行排序,将排好序的片段写到外存中,这样每个片段的数据时有序的
- 第二阶段,利用归并算法,归并第一阶段生成的片段,直到得到一个包含所有数据的大归并段为止
- 应用实例
- 假设要对外存中4500个记录进行排序,但是内存中只能存放750个记录
- 在第一阶段,每次读取750个记录到内存进行内排序,得到6个有序的片段(片段1-片段6)
- 第二阶段,利用归并算法对所有片段进行两两合并,具体方法如下
- 将内存分为三部分,每部分可保存250个记录,其中两个输入缓冲区,一个输出缓冲区
- 首先对第一阶段得到的片段1和片段2进行归并,从片段1和片段2各读取250各记录到输入缓冲区,对两个输入缓冲区的记录进行合并,合并方式类似于归并排序,就不详细说了
- 如果输出缓冲区满,则把输出缓冲区的记录写到磁盘(写到一个大小为1500的文件,因为片段1和片段2合并后大小为1500)
- 如果某个输入缓冲区为空,则从对应的片段再次读取250个记录
- 重复以上过程,直到所有记录被合并成一个大片段,此时排序完成
1.4 解答关键
- 主要使用分而治之的思想解决大数据问题。即通过哈希函数将大任务分流到机器或分流成小文件
- 另外,解决大数据问题常用的工具有hashMap和bitMap
二、大数据问题
2.1 对10亿个IPv4的IP地址进行排序,每个IP只会出现一次
- 想法,IPv4地址长度为4字节,因此可以把IPv4地址转化为32位无符号整数unsigned int
- 解答步骤
- 申请长度为 2 32 2^{32} 232的bitMap,即长度为 2 32 2^{32} 232的bit类型数组,数组的每个位置的值要么是0要么是1
- 遍历给出的10亿个IPv4地址,把每个IP地址对应的bitMap位置(通过IP -> unsigned int得到位置)设为1
- 遍历bitMap,相应位置为1则得到一个IP地址(通过unsigned int -> IP)。遍历完成后得到的IP地址序列就是有序的
- 空间复杂度分析
- 本解法的空间主要消耗在bitMap,一个长度为 2 32 2^{32} 232的bit类型数组占用的内存空间大约是512M
2.2 对10亿人的年龄进行排序
- 想法,年龄是一个有范围的整数,可以假设年龄分布在[0, 200)的区间内
- 解答步骤
- 申请一个大小为200的数组
- 遍历给出的10亿个年龄记录,根据年龄将其放进对应的数组位置
- 遍历数组,从数组位置中取出所有的年龄记录。最后得到的年龄记录序列就是有序的
2.3 有一个包含20亿个全是32位整数的大文件,在其中找到出现次数最多的数,内存限制2G
- 想法
- 最简单的方法是使用hashMap,其key为文件中的整数,占用4字节;其value为出现次数,由于一共有20亿个整数,因此可能的最大出现次数为20亿,value也需要占用4字节。根据前面的分析,一个(key, value)对需要8字节,极限情况下,有20亿个(key, value),占用大约16G内存,明显超出了题目限制。
- 内存优先的情况下解决大数据问题,我们需要分而治之,具体步骤如下
- 解答步骤
- 使用哈希函数将大文件中的整数分流到多个小文件中
- 统计每个小文件中整数出现次数,得到每个小文件中出现次数最多的整数
- 比较所有小文件中出现最多的整数,得到全部小文件中出现最多的整数
2.4 32位无符号整数的范围是0~ 2 32 2^{32} 232-1。现在有一个正好包含40亿个无符号整数的文件,所以在整个范围中必然有没出现过的数。可以使用最多10M的内存,只用找到一个没出现过的数,该如何找?
- 想法
- 看到题目后第一想法是用bitMap做,长度为 2 32 2^{32} 232的bitMap占用内存为512M,远远超过了题目限制,因此不可行
- 解决这个问题的方法还是分而治之,一个32位无符号整数占用4个字节,计算10M的内存可以保存多少个32位无符号整数,然后我们把0~ 2 32 2^{32} 232-1的区间分成多个小区间,每个小区间都可以装进10M内存中
- 解答步骤
- 把大区间按照前面所述的方法分成多个小区间
- 如果某个小区间不满,则肯定有至少一个没出现过的数属于这个小区间
- 针对不满的小区间,使用bitMap找到没有出现的数,问题解决
2.5 某搜索公司一天的用户搜索词汇量是海量的,假设有百亿的数据量,请设计一种求出每天最热100词的可行办法
- 想法
- topN问题可用堆来解决
- 解答步骤
- 使用哈希函数把百亿条记录分配到不同的计算节点
- 根据计算节点的内存大小限制,分配到每个计算节点的记录再次使用哈希函数分流到不同的小文件
- 处理每个计算节点的每个小文件,每个小文件进行词频统计,维护一个大小为100的小根堆得到每个小文件的top100搜索词
- 由每个计算节点上所有小文件的小根堆,得到每个计算节点的top100,这部分也由小根堆实现
- 由每个计算节点的小根堆,得到百亿记录中的top100,这部分同样由小根堆实现,问题解决
2.6 工程师常用服务器集群来设计和实现数据缓存,以下是常见的策略。1,无论是添加、查询还是删除数据,都先将数据的id通过哈希函数转换成一个哈希值,记为key。2,如果目前机器有N台,则计算key%N的值,这个值就是该数据所属的机器编号,无论是添加、删除还是查询操作,都只在这台机器上进行。请分析这种缓存策略可能带来的问题,并提出改进的方案
- 想法
- 这种缓存策略的问题是如果增加或删除机器,数据迁移的代价很大
- 改进方案是一致性哈希
- 解答步骤
- 假设哈希函数的哈希值范围是[0, MAX]
- 把0到MAX首位相连形成一个环
- 将每台机器的id通过哈希函数转换成一个哈希值,找到这个哈希值在环上的位置
- 对于数据,也将id通过哈希函数转换成一个哈希值,找到这个哈希值在环上的位置,然后从该位置顺时针移动,遇到的第一个机器,就是这个数据所属的机器
- 优势
- 一致性哈希的优点在于增加或删除计算节点时,只会影响到部分的数据,而不是像普通方法那样会影响到所有的数据
- 至于为什么只会影响到部分数据,把环画出来分析一下就知道了,比较简单