背景
- 判断单词拼写是否正确
- 判断垃圾邮件
- url去重
- 缓存穿透问题(访问不存在的元素会穿透redis缓存而始终访问mysql,造成mysql压力过大)
需求
海量数据中查询某个字符串是否存在
平衡二叉树
红黑树、AVL,b/b+树, 跳表
时间复杂度是O(log2(n))
10亿节点最多比较30次
散列表(哈希表)
根据key计算在表中存储的位置
hash函数
Hash(key)–>addr; 两个key被隐射到一个地址称为冲突
负载因子:数据储存元素个数/数据长度 (越小冲突越小)
冲突处理
- 链表法:java/c++ 标准库都是链表法
- 头插法:redis等都是这样,因为对于数据库,最新的数据是最常访问的
- 尾插法:算法有可能退化成o(n)
- 可以将过长的链表(经验值:256)转化成红黑树(java)
- 开放寻址法
- 搜索addr,addr+a(i,1) , addr+a(i,2)…
- 根据a,如果a是线性,容易哈希聚集,效率不行,如果a是二次,则会讲聚集延后 (都不怎么用)
- 另外还可以使用双重哈希来解决哈希聚集的现象(原理略)
BloomFilter
概率性数据结构
高效地插入和查询
能确定某个字符串一定不存在或可能存在
不存储具体数据–>占用空间小,查询结果存在误差但是误差可控
不支持删除操作
构成
bit数据+n个hash函数
每个hash把key映射到一个bit,多个hash映射到多个bit,可以告诉我们字符串是否存在
加入一共64bit
hash(val)=173
- 174%64 = 45
- j=45%8
- i=45/8
插入时把每个hash对应的bit设为1
查询时,检测每个hash对应的bit是否都为1,如果有0则一定不存在,否则可能存在
为什么不能删除?因为如果删除,则可能把其他key的bit也置为0
应用场景
用于判断某个key一定不存在,同时允许判断有误差的情况
- 解决缓存穿透
- 热key限流
应用分析
多少个hash?多少bit?预期储存多少元素?如何控制误差?
公式:
n -- 预期放多少个元素
p -- false positive possibility (判断为存在但实际不存在的概率)
m -- 位图所占空间
k -- hash个数
n = ceil(m / (-k / log(1 - exp(log(p) / k))))
p = pow(1 - exp(-k / (m / n)), k)
m = ceil((n * log(p)) / log(1 / pow(2, log(2))));
k = round((m / n) * log(2));
https://hur.st/bloomfilter
先确定n和p,再计算m和k
选择hash
- 计算速度快
- 强随即缝补(等概率、均匀地分布在整个空间)
- murmurhash1, murmurhash2, murmurhash3, siphash (redis6.0使用,rust使用),cityhash
- siphash主要解决字符串接近的强随机分布性
- 为什么hash实现过程当中经常出现i*31?
- i*31 == i << 5 - i 计算快
- 31是质数,hash随机分布性好(好于17和101)
构造k个hash函数
取两个hash函数,hash1(key), hash2(key)
hash[i] = (hash1 + hash2*i) % m
如果一定要支持删除操作怎么办
添加第二个布隆过滤器,用于记录已删除,先走之前那个,再查第二个,如果查到则表示这个元素已经被删除(也有误差)
解决缓存穿透
场景
- 访问redis,如果redis不存在走2
- 访问mysql,如果存在返回数据
缓存穿透:当数据不存在时一定会走mysql,数据库压力很大
发生原因:黑客利用伪造数据共计,或者业务bug造成大量重复请求不存在的数据
解决方法:在server增加一个bloomfilter,把mysql中存在的可以都存进去,这样就能在server端判断不存在的数据了
分布式一致性hash
把hash空间组织成一个虚拟的圆环,圆环大小是2^32
分布式缓存
比如memcached集群
一开始有3台机器,我们用hash(key)%3分区
后来又加了一台机器,变成hash(key)%4分区,可是原来的映射被改变了(没有一致性)
一致性hash
一个圆环
每个服务器落在hash(ip:port)%2^32
每个key计算hash(key)%2^32,然后顺时针找到第一台服务器
增加节点
增加节点可以把原来之后那个节点的一部分数据分给新节点,其他节点不影响
怎么消除影响:数据迁移
如何保证每个服务器均匀分布?
不均匀的原因是什么:节点太少
解决方法: 虚拟节点
每个服务器都有多个虚拟节点hash(ip:port:1), hash(ip:port:2) … hash(ip:port:30)
把虚拟节点映射到真实节点