哈希函数
1. 哈希函数的特点
输入域无穷大,输出域有限;
相同的输入定然导致相同的输出;
不同的输入也可能导致相同的输出(哈希碰撞);
虽然存在一定的哈希碰撞,但如果存在大量的输入数据那么哈希的输出域上的每一个点基本是均匀增长的(好的哈希函数它的增长于输入数据本身的质量无关,即使输入数据本身存在一定的规律,也应该保证每个点均匀增长)。
2.哈希函数的原理
数学上的疏密数
3.哈希函数的用处
根据哈希函数的特性就可以知道,它用来处理数据的分类过多时产生的问题。
因为经过哈希函数处理后的数据在输出于s上是均匀分布的,那么处理后的数据都对m取余后,在m上也是均匀分布的。且如果s和m的差值越大(s>> m),这个规律越明显。
如:硬盘上存储了20亿个整数,内存有2G,请在该种条件下找出出现频率最高的数字。
在最差情况下(即20亿个数字都不相同),我们需要20亿块区域来存储数据;如果使用map结构(假设一个map结构需要8Byte),那么一共需要16G内存。
2G内存按8Byte划分可以划分为16MB的区域,将20亿个数字通过哈希后得到值对16MB进行取余然后进行统计,对这16MB里的数据取频率最高的,然后对这个区域里的数字再进行统计。
4.哈希算法的简单实现思路
https://cloud.tencent.com/developer/article/1079392
哈希表
经典实现
桶加单链表结构,一条数据经过哈希函数处理后得到一个值,该值对桶的长度取余,得到一个新的值,根据该值决定这条数据在哈希表中的位置,如果该位置上已经存在数据,就通过链表新建节点连接在该哈希表的后面。
- 根据 key 计算出它的哈希值 h。
- 假设桶的长度为 n,那么这个键值对应该放在第 (h % n) 个桶中。
- 如果该桶中已经有了键值对,就使用开放寻址法或者拉链法解决冲突。
经典实现的哈希表中在扩容时会存在问题。如果哈希表中的链表过长,这时就需要扩容,如果扩容后的哈希表的长度是之前的2倍(一般都是2倍),那么链表的长度就是之前的一半,所以扩容后很长一段时间都不需要扩容。但一旦需要扩容,那么就需要将全部的数据重新计算,然后重新放入哈希表中,对性能的影响较大。
在Java中对哈希表扩容的问题有所优化。Java在扩容时使用离线技术,用户可以在扩容的同时对之前的哈希表进行操作,不占用用户的在线时间,所以在实际使用中可以认为哈希表的增删改查操作的时间复杂度都是O(1)。
布隆过滤器
布隆过滤器一般用于去重或者用于黑名单的检测,但存在一定的失误率,可能会将不属于黑名单的对象认为是黑名单的对象。
介绍布隆过滤器,首先要介绍位图的概念。
当我们的数据量很大,比如说我们有100亿个数据,那么可能所有的整数都出现了一遍,这时我们就不需要将100亿个数据全部存储,只需要确定某个数是否存在即可。又因为一个数只能有两种状态,即在或不在,所以是一个数字只需要一个Bit就可以确定,可以节省很多空间(这样的话一个int可以有32个状态,我们保存所有的整数范围仅仅需要512MB即可)。
那么怎样在位图上确定一个数字是否存在呢?
int arr[100];//可以表示100*32个数字的状态
int k = 2789;
//现在要确认k这个数是否存在
int a = 2789 / 32;//确定数字k的状态在arr[a]上存储
int b = 2789 % 32;//确定数字k的状态在arr[a]上的第b位存储
int kStatus = a & (1 >> b);
介绍完位图后我们来看布隆过滤器。
假设有一个长度为m的位图,有k个哈希函数,100亿个url。
每一个url可以通过一个哈希函数确定一个值k,将位图上k对应的位置变为1,因为有多个不同的哈希函数,所以一个url会将位图上的多个位置变为1。
布隆过滤器存在失误率的原因
由布隆过滤器自身的结构和哈希算法本身的特性可以知道,如果位图本身的长度过小,或者说输入对象过多,导致位图上绝大部分位置都已经为1了,这时候你再输入一条数据,那么由很大的可能会被哈希映射到已经被涂黑的位置,被系统认为这条新数据是黑名单上的成员,也就是说如果一个url本来不属于黑名单,可能会被误判;而如果一个url属于黑名单,是绝对不会被误判的,因为哈希函数本身的特性决定相同的输入一定会导致相同的输出,一个url经过多次哈希后得到的值一定相同,所以可以保证一个url如果再黑名单上,那么它经过哈希后一定会再次落到已经被为1的位置。可以说布隆过滤器的原则就是宁可枉杀1000,绝不放过1个。
布隆过滤器的使用范围
黑名单系统、垃圾邮件过滤系统、去重系统。
布隆过滤器的大小m、哈希函数个数与实际失误率怎样确定
m的值与输入数据的样本量n和期望的失误率p有关。
假设n = 100亿,p = 0.0001
那么
在实现布隆过滤器时,m和k的取值一般都选择向上取整,实际的失误率为
一致性哈希原理
一致性哈希原理主要用于服务器(主要是分布式数据库)的负载均衡和数据迁移。
服务器架构(没有一致性哈希的情况)
负责处理request请求的服务器是怎样将请求映射到后台服务器(如果存在多台服务器)的呢。为了保证负载均衡,将受到的请求哈希后取余,均匀分布到后台服务器上,但当需要增加后减少后台服务器时,需要进行数据迁移,此时必须将所有数据重新哈希一遍,导致服务器性能降低。所以传统的服务器架构在进行数据迁移时的代价非常大。
使用一致性哈希时的情况
一致性哈希可以很好的解决这两个问题,即一致性哈希的优势有负载均衡和数据迁移时的代价小。
简单来说,一致性哈希也是一种哈希算法,它希望在服务器进行增减时,尽量减少需要改变的映射关系。
首先对哈希后的后台服务器进行排序,当前台服务器接收到请求时,同样将请求进行哈希,得到的值与后台服务器的哈希值进行比较(一般用二分),取比该值大且两个哈希值的差最小的服务器。如果不存在哈希值比请求的哈希值还大的服务器,则选择从0开始选择距离最近的服务器。最后形成的是一个哈希环。
当需要减少服务器时,只需要将请求经过哈希函数后得到的哈希值在某一范围内的数据进行迁移即可。
当需要增加服务器时,同样只需要改变哈希值在某一范围内的数据即可。
虚拟节点技术
现在我们需要处理的问题就是:怎样做到负载均衡呢?除了增减服务器时的情况需要考虑,还应该考虑初始化时分配服务器的情况,即后台服务器第一次通过哈希函数得到的哈希值是否均等分布。我们都知道,后台服务器的数量与我们的哈希表的长度相比肯定是很少的,而哈希函数设计的一个基础原则就是要保证在输入数据的样本量很大的情况下,输出域的频率基本是均衡增长的,所以哈希函数不能保证输入数据很少的情况下它们均等分布,相反它们可能距离很远。
解决这个问题可以使用虚拟节点技术。
我们构造一个路由表:服务器A、B、C 各自有1000个虚拟节点。
A | a1, a2, a3 ……a1000 |
B | b1, b2, b3 …… b1000 |
C | c1, c2, c3 …… c1000 |
将这些虚拟节点根据哈希函数分布在之前得到环上,此时请求就不再是寻找服务器,而是在寻找虚拟节点了,寻找虚拟节点的原理与之前提到的寻找服务器的过程相同。找到虚拟节点之后,根据上面的路由表找到对应的服务器就可以了。因为哈希函数的性质决定了数据量越多,它们的分布就越均衡,所以在增加服务器时,只需要增加新的虚拟节点就可以了;而减少服务器时同样只需要减少该服务器对应的虚拟节点即可。
如果我们的后台服务器性能存在差异,可以为性能好的服务器分配更多的虚拟节点,以此做到性能均衡。
接下来解决怎样为一台服务器生成多个虚拟节点
1. 可以对一台服务器使用多个哈希函数
2. 为服务器的主机名加上后缀,如服务器A可以变为A1,A2,A3……,对这些加上后缀的字符串进行哈希后得到虚拟节点。