Hash学习笔记
其实就是去重, 要如何从大量的数据中查询某个字符串是否存在
BST 平衡二叉树
对于平衡二叉树来说, 查找的时间复杂度是
也就是 100万个节点, 最多比较20次; 10亿个节点最多比较30次
其实就是利用二分法快速排除一半的数据达到快速搜索
这个时间复杂度已经很不错了
对于比较字符串效率是不高的
散列表
就是hash和数组的组合
根据key计算key在数组中位置的数据结构; 是key和其他所在存储地址的映射关系
hash函数
通过一个算法来计算出一个hash值, 可以简单的想象成一个函数接受一个x然后生成一个值
Hash(key) = addr, 是尽可能的让不相同的key值的hash值不相同, 但是毕竟是算法不能完全保证产生的哈希值完全相同, 就出现了hash冲突
- 计算速度快
- 强随机分布(等概率、均匀分布在整个地址空间)
murmurhash1,murmurhash2,murmurhash3,siphash(这个是redis6使用, 而且rust和大多数语言选用的hash算法去实现hashmap),cityhashsiphash主要解决字符串接近的强随机分布性
murmurhash2和cityhash都是同一个大佬搞出来的, 牛的啊
hash函数的测试可以参考这个repo: aappleby/smhasher: Automatically exported from code.google.com/p/smhasher (github.com)
ciphash
主要解决了字符串接近的强随机分布性
比如说uid一般都是会是一些比较接近的字符串比如: 10001, 10002, 10003, 这样子能有效的解决hash聚集
负载因子
hash表中会有一个负载因子来描述hash冲突的一个程度, 可以理解成密度
通常的计算方式:
负载因子越小, 冲突越小; 负载因子越大, 冲突越大
c++中的map、java中: 0.6左右
redis: 1
冲突处理
也就是如何解决hash冲突
1. 链表法
绝大多数的解决办法都是通过这个, 比如 java, redis, c++
就是如果发现hash冲突了, 就将冲突的元素用链表链接起来
一般来说链表使用头插法, 因为普遍认为最近更新的可能最近会需要用到
但是会有一种非常极端的情况, 就是假如他全冲突了或者说冲突元素非常多, 导致了冲突链表过长, 会导致查询的时候复杂度是O(n), 这个时候可以把链表转换为红黑树
至于要超过多少个节点的时候要转换成红黑树呢这个就取决于个人了 (反正jdk8里是8)
2. 开放寻址法
将所有的元素存放在hash表的数组中, 不使用额外数据结构, 使用线性探查的思路解决
- 当插入新元素时, 使用hash函数在哈希表中定位元素位置
- 检查数组中该槽位索引是否存在元素, 如果该槽位是空, 则插入, 如果不为空执行3
- 在2检测的槽位索引上加一定步长接着检查2
加步长的方法:
i + 1,i + 2,i + 3,i + 4,...,i + n- , , , ,
这两种方法都会导致hash聚集, 就是近似的hash值也近似, 数组槽位也是靠近的, 形成了聚集;
第一种同类聚集冲突在前, 第二种只是将冲突延后
可以使用双重hash来解决hash聚集
.net的方式`Hk(key) = [GetHash(key) + k * (1 + (((GetHash(Key) >> 5) + 1) % (hashSize - 1)))] % hashSize
执行了hashSize次数的探查之后, 哈希表中的每一个位置都有且只有一次被访问到
hash函数的实现中为什么会出现i * 31?
i * 31 => i * (32 - 1) => i * (1 << 5 - 1) = i << 5 - i- 31是个质数, 在哈希的过程中产生的值更均匀, 就是随机分布会更好一些,
1731101都是, 但是31表现的更好
布隆过滤器
布隆过滤器是一种概率型数据结构, 它的特点是高效地插入和查询, 能确定某个字符串一定不存在或者可能存在
不存储具体数据(通过n个hash函数, 在n个比特位中将他标识为1), 所以占用空间小, 查询结果存在误差, 但是误差是可控, 同时不支持删除操作
利用位图
将取余运算转换成二进制位运算, 这是一个优化
m% =m&
注意这个重复的, 对于这个槽位来说不能判断
原理
检索时, 再通过k个hash函数运算检测位图的k个点是否全为1, 如果全是1只能证明可能存在; 如果有不为1的点, 那么认为该key不存在;
只能判断可能存在的原因是, 无法判断是哪一个字符串或者哪个哈希函数映射的, 也是利用这个特性来实现相应的业务场景
不支持删除
因为不知道1是哪个hash函数或者哪个字符串的, 如果支持删除操作的话需要设置0务必会影响其他的string
应用场景
通常用于判断某个key一定不存在的场景, 同时判断存在时有误差的情况
1. 缓存穿透的解决
-
缓存穿透
利用redis和mysql都没有这个数据, 黑客可以利用此漏洞导致查询不走缓存, 导致数据库压力过大, 如此以来整个系统陷入瘫痪
-
读取步骤
2.1 先访问redis, 如果存在直接返回; 如果不存在走2.2
2.2 访问mysql, 如不存在直接返回, 如果存在走2.3
2.3 将mysql存在的key写入redis
-
解决方案
3.1 在redis端设置
<key, null>键值对, 以此避免访问mysql, 缺点很明显过多的话占用内存可以给key设置过期时间, 停止攻击最终由redis自动清除这些无用的key
3.2 在server端存储一个布隆过滤器, 将mysql包含的key放入布隆过滤器中; 利用布隆过滤器的特性能够过滤一定不存在的数据
2. 热key限流
a.
一个业务需求: 统计key是否为热key, 如果访问过多做限流
一定时间内统计key的访问次数, 如果这个频次达到一定的阈值, 将这个key放到布隆过滤器中
每次来访问key的时候都先来查询布隆过滤器, 这个key是否是个热key, 如果不在就允许直接访问(用到了布隆过滤器能判断一定不存在的特性)
如果在这个布隆过滤器中, 因为布隆过滤器中存在一定误差, 如果发生误差就直接让用户操作失败
b. 类似黑名单, 爬虫也是用这个
应用分析
- 应该选择多少个hash函数
- 要分配多少位图空间
- 预期存储多少元素
- 如何控制误差
n -- 布隆过滤器中元素的个数, 像上面那个例子 只有 str1 和 str2 两个元素, n = 2
p -- 假阳率, 再 0-1之间, 可能存在的误差
m -- 位图所占空间
k -- 哈希函数的个数
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));
确定n和p
可以参考这个网站选择对应的: hur.st/bloomfilter
在使用布隆过滤器之前
- 首先确定
n和p - 再通过公式计算获得
m和k
选择hash函数
采用双重hash
#define MIX_UINT64(v) ((uint32_t)((v>>32)^(v))
uint64_t hash1 = MurmurHash2_x64(key, len, Seed);
uint64_t hash2 = MurmurHash2_x64(key, len, MIX_UINT64(hash1));
for (int i = 0; i < k; i++) // k是hash函数的个数
{
Pos[i] = (hash1 + i * hash2) % m; // m是位图的大小
}
这就生成了k个hash函数
如何支持布隆过滤器删除
删除操作也是存在着误差
准备两个布隆过滤器, 如果要删除1中的某个元素, 将这个元素添加到布隆过滤器2
布隆过滤器2就是记录已经删除的元素, 如果第二次要判断是否有这个元素, 首先判断第一个再来判断第二个, 第二个中如果有就是已经删除了的
分布一致性hash
最先是用来解决分布式缓存的问题
希望把数据均衡的存储在缓存节点中
hash(key) % 3(节点数量) 但是有个非常明显的问题随着服务访问提升增加节点的时候, hash算法变成了 hash(key) % 4 这样导致了数据混乱了对应不上了
这样子, 分布式一致性哈希就诞生了
hash(key) %
想象成一个哈希圆环, 可以想象成一个循环数组
这样子增加新的缓存节点也不会影响原来的存储
但是这个情况太理想了, 在上面这个情况下是均匀分布的, 因为哈希算法的强随机性可能会导致某些节点特别的集中
就会造成数据存储的不均衡
解决办法就是增加虚拟节点
产生这种分布密集的原因是哈希随机性和节点太少了, 必然会出现这种情况, 如果节点足够多, 能均衡的分布, 所以我们就增加虚拟节点
为每一个节点生成多个虚拟节点
我们只需要根据虚拟节点找到真实ip
对于增加节点造成部分数据的影响, 需要数据迁移
redis集群是怎么做数据迁移的呢
数组槽位, 通过hash映射到S1,S2,S3中