哈希
认识哈希函数
性质:
- 输入域无限,输出域有限
- MD5(02^64-1)、SHa1(02^128-1)
- 相同的输入值一定有相同的输出值(不随机)
- 相同的输出值不一定是相同的输入值(哈希碰撞,概率很低)
- 映射有很平均的离散型(均匀)
- 一堆数取哈希值并%m,得到的一堆数在0-(m-1)上均匀分布
题目1
给定一堆无符号整数,范围如上图,有40亿个,求出现最多次数的是哪个数(限制:1G内存)
思路:
经典方法:使用哈希表key存数,value存次数
内存不够:key和value存数int都是4字节,一条记录至少8字节(还不包括索引等内存)
最差情况:40亿条记录都不一样,320亿B的内存大约是32G
哈希表内存增加是由于不同的数据,如果记录都是相同的,改变的只是value,内存不会骤增
优化思路:
把40亿个数取哈希,再分别%100,得到40亿个范围在0-99的数,因为相同的数取哈希得到的结果一样,所以再%100结果也是一样的,而且这些数可以看作均匀分布再0-99这个范围,然后准备一个文件(相同的数一定进同一个文件),先放结果为0的数,利用哈希表得到在结果为0中出现次数最多的数并记录该数以及出现的次数,清除文件内容并得到结果为1中出现次数最多的数并记录该数以及出现的次数,周而复始,统计完0-99这个范围上每个数中的结果,再统一作一次比较(比较一百个数出现的次数),得到最终结果,这样内存空间就只占用了32G/100=0.32G(大约)
认识哈希表的实现
经典哈希表的实现:哈希-取模-放置
时间复杂度:哈希O(1)、取模O(1)、数组寻址O(1)、链表中取O(k)(k较小,可以认为是O(1))、单次扩容代价O(logN)
为什么说哈希表在使用上的各种操作效率是O(1)?(理论上不是O(1),理论上是O(logN))
- 因为可以控制k的大小(链表的长度),减少扩容代价(k^n=N)(逼近O(1))
- Java虚拟机JVM可以使用离线扩容技术,不占用用户在线时间,降低哈希表的使用代价
不同的语言还有不同的优化方式:java红黑树
设计RandomPool结构
准备两个HashMap 保证index和str能互相取到以及一个变量size(只用一个HashMap做不到时间复杂度都是O(1))
getRandom可以使用系统带的random函数来随机获取index从而返回结构中的任何一个key
注意移除方法:在移除时不能单纯的删掉HashMap中的元素,还要保证连续性(即不要在中间出现洞,会影响getRandom方法)
public class Pool<K>{
private HashMap<K,Integer> keyIndexMap;
private HashMap<Integer,K> indexKeyMap;
private int size;
public Pool(){
this.ketIndexMap=new HashMap<K,Integer>();
this.indexKeyMap=new HashMap<Integer,K>();
this.size=0;
}
public void insert(K key){
if(!this.keyIndexMap,containsKey(key)){
this.keyIndexMap.put(key,this.size);
this.indexKeyMap.put(this.size++,key);
}
}
public void delete(K key){
if(this.keyIndexMap.containsKey(key)){//先判断是否有该key
int deleteIndex=this.keyIndexMap.get(key);
int lastIndex=--this.size;
K lastKey=this.indexKeyMap.get(lastIndex);
this.KeyIndexMap.put(lastKey,deleteIndex);
this.keyIndexMap.remove(key);
this.indexKeyMap.put(deleteIndex,lastKey);
this.indexKeyMap.remove(lastIndex);
}
}
public K getRandom(){
if(this.size==0){
return null;
}
int randomIndex=(int)(Math.random()*this.size);//0~size-1
return this.indexKeyMap.get(randomIndex);
}
}
详解布隆过滤器
一个集合只有添加和查询操作,没有删除行为(黑名单、爬虫去重问题)
需要较快的查询时间(比硬盘快)
牺牲掉一点的准确性去换取速度
这里的失误是指:比如黑名单,不是黑名单但我给我反馈是在黑名单中的(错杀)
不会有是黑名单但查出来不是黑名单的失误
布隆过滤器虽然可以减少内存的使用,但失误率不可避免,但可以通过设计降低失误率
比特类型的数组
int[] arr=new int[10]
//长度为10的int数组是32bit*10 ->320bits
arr[0]可以表示0-31位的信息
arr[1]可以表示32-63位的信息
…
即一个长度为10的整型数组可以表示320个bit的bit类型数组
int i=178;//想取得178个bit的状态
int numIndex=i/32;//去哪个数上面找
int bitIndex=i%32;//去这个数上的哪一位找
int s=((arr[numIndex]>>(bitIndex))&1);//拿到178位的状态
arr[numIndex]=arr[numIndex]|(1<<(bitIndex));//把第178位的状态改成1,其他位置不变
arr[numIndex]=arr[numIndex]&(~(1<<bitIndex));//把第178位的状态改成0,其它位置不变
黑名单实际操作过程(url)
- 准备一个位图,长度为m,实际占用m/8个字节数(byte)
- url1通过hash函数得到out1然后%m得到相应的位置并做标记,然后通过下一个hash函数得到out2然后%m得到相应的位置并做标记,设定了几个hash函数就做几次这样类似的操作(k个hash函数就做k次),每一个url都做这样的操作
- 查url时,urlx按上述的hash函数依次调用并%m,只有结果全是1(即全都做了标记),才认为该url在黑名单中,只要有一个结果不是1就没加入过该url,因此不会出现加入过但查询结果显示没加入过这样的失误;如果m较小且数据量较大,则有可能出现没加入过黑名单的url查询结果为加入过黑名单
- 关键因素:m的大小,如果m很大,失误率就会很低
- 另外一个因素:k的大小,k实际与样本量和m相关(也可以说是根据m的大小来决定),有点类似于采集指纹
实际需要的参数就只有两个:n样本量和p失误率
每个样本的大小只要满足能传入hash函数就可以了
面试场合有相关的问题要问是否允许一定的失误率
三个公式
100亿的数据最后大约只需要26G(原本需要640G)(一台服务器就能搞定)
k值最后算出来向上取整
若需要26G而服务器给了32G,就有第三个公式可以算实际的失误率
详解一致性哈希原理
分布式:谈论数据服务区怎么组织的问题
逻辑层任何一个实例都是等效的(从数据库获取数据,不需要维持自己特有的数据)
数据端服务器(分布式)(可以承载更大的数据量)
假设有三台数据段服务器,每一台上面的数据都是独立的,现在要存一个数据,先经过逻辑层,再根据数据的key值经过hash函数的计算然后%3得出一个结果,结果范围0-2,分别对应三个数据库
查该数据时,也是同样的经过逻辑层(不同的逻辑服务器没有区别),然后再经过对key值进行hash函数的计算然后%3得到我应该从哪个数据服务器上取数据
底层数据段服务器组织是否负载均衡是要看高频的key、中频的key、低频的key有没有足够的数量来决定的,只要都有足够的数量,那就是能够满足负载均衡
key的选择很重要:拿什么样的key来做这样的数据划分,要选取种类多的key,如姓名和身份证号码等,像国家名就不适合做,中美的查询频率最高,高频的数量就两个,如果有三台服务器,至少有一台数据服务区压力很低,没有实现负载均衡
如果突然数据量变大,数据库服务器不够用了(分布式)
如果增减数据库意味着全量的数据需要重新对key进行hash函数的计算和取模的操作(模的数量是数据库服务器的数量)
如何降低数据迁移的代价?
一致性哈希
一致性哈希是没有取模这一操作的
假设现在使用MD5来做hash,把hash的结果范围(0-264-1)想象成一个环,即264-1的下一位是0
把能区分机器的信息作为机器的hash值(将该信息进行hash函数的计算并放到环中)
在逻辑层把每台机器的hash值保存起来(有序数组)(每一个逻辑服务器维持的数据一样),进来想要保存一个数据时,对数据的key进行hash函数的计算,然后在逻辑层通过二分法找到hash离他最近的机器(顺时针最近,即大于等于的最近的)
现在加一个数据服务器,比如是m4,则只需要将原本一部分归属于m3服务器的数据分给m4,减少数据服务器的时候也是同样的道理,不用对全量的数据进行迁移
但是存在两个问题:
- 机器数量很少的时候,可能做不到一上来环就是均分的(hash函数均分的前提是数据量足够大)
- 即便数据服务器很少的时候能把环均分,一旦数据服务器增加或者减少的时候,马上会变成负载不均衡
如何解决以上问题?
虚拟节点
比如,三个服务器分别构建一千个虚拟节点,环由这些虚拟节点去抢,按比例去抢环
增加服务器时,也让第四个服务器由1000个虚拟节点去抢环,该抢谁的数据就抢谁的,数据的迁移由
按比例抢就能很好的解决初始时不均和增减服务器时不均的问题
h函数均分的前提是数据量足够大)
- 即便数据服务器很少的时候能把环均分,一旦数据服务器增加或者减少的时候,马上会变成负载不均衡