散列表,也叫哈希表,是一种能够通过给定的关键字的值直接访问到具体对应的值的一个数据结构
散列表用的是数组支持按照下标随机访问数据的特性,所以散列表其实就是数组的一种扩展
散列表有两种用法:一种是键值和值相同,称为集合;另一种是键值和值不同,称为映射
Hash函数,也叫散列函数,是一种把任意长度的输入变换成固定长度的输出的函数,Hash函数的作用是通过关于键值的函数,将数据映射到内存存储中一个位置来访问
对于不同的输入可能会散列成相同的输出,导致无法从散列表来唯一确定输入值,这种情况称为Hash冲突
hash的优点是计算速度快、检索效率非常高,可以一次到位,而且可以实现安全加密,具有强随机分布的特点
murmurhash1,murmurhash2,murmurhash3,siphash(redis6.0当中使⽤,rust等大多数语言选用的hash算法来实现hashmap,它主要解决字符串接近的强随机分布性),cityhash 都具备强随机分布性
Hash有很多的应用场景,如:存储商品、淘宝短链接、分布式session、用户注册、MySQL中的Memory存储引擎和自适应Hash等等
负载因子是哈希表中用来衡量哈希表的空/满程度的一个比值,它的值等于哈希表中的元素个数除以哈希表的大小
负载因子越大表示哈希表越满,则发生哈希冲突的可能性就越高,查询效率也就越低
负载因子越小,说明哈希表越空,浪费的空间就越多
因此要选择一个合适的负载因子,一边默认为0.75
布隆过滤器是一种高空间利用率的概率型数据结构,它由一个二进制向量(即位数组)和一系列随机映射函数(即哈希函数)组成
布隆过滤器可以用于判断一个元素是否可能存在于一个集合中,如果返回否,则该元素一定不存在;如果返回是,则该元素有一定概率存在
布隆过滤器不存储具体数据,所以占用空间小,查询结果存在误差,但是误差可控,同时不支持删除操作
当一个元素加入位图时,通过 k 个 hash 函数将这个元素映射到位图的 k 个点,并把它们置为 1;当检索时,再通过 k 个 hash 函数运算检测位图的 k 个点是否都为 1;如果有不为 1 的点,那么认为该 key 不存在;如果全部为 1,则可能存在。
为什么不支持删除操作:
在位图中每个槽位只有两种状态(0 或者 1),一个槽位被设置为 1 状态,但不确定它被设置了多少次,也就是不知道被多少个 key 哈希映射而来以及是被具体哪个 hash 函数映射而来
布隆过滤器通常用于判断某个 key 一定不存在的场景,同时允许判断存在时有误差的情况
缓存穿透是指有恶意请求查询不存在的数据,导致缓存和数据库都无法命中,从而给数据库造成压力的问题
解决方法是将所有可能存在的数据缓存放到布隆过滤器中,当用户请求一个数据时,先用布隆过滤器判断是否存在,如果不存在就直接返回,避免查询缓存和数据库
热key限流是指对于一些访问频率很高的key,采取一些措施来减少对缓存或数据库的压力,比如设置过期时间、分布式锁、限流算法等
布隆过滤器可以用于热key限流的一个场景是,当有大量的请求查询一个不存在的key时,如果直接访问缓存或数据库,可能会造成缓存穿透或雪崩,这时候可以使用布隆过滤器来快速判断这个key是否存在,如果不存在就直接拒绝请求,从而减少无效的查询
在实际应用中,该选择多少个 hash 函数?要分配多少空间的位图?预期存储多少元素?如何控制误差?
公式如下:
n−−预期布隆过滤器中元素的个数,如上图只有str1和str2两个元素那么n=2p−−假阳率,在0−1之间0.000000m−−位图所占空间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))n -- 预期布隆过滤器中元素的个数,如上图 只有str1和str2 两个元素 那么 n=2\\ p -- 假阳率,在0-1之间 0.000000\\ 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))\\ n−−预期布隆过滤器中元素的个数,如上图只有str1和str2两个元素那么n=2p−−假阳率,在0−1之间0.000000m−−位图所占空间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))
在实际使用过程中,需要先确定n和p的值,通过上面的公式计算得出m和k,可以在这个网站通过可视化来选择出合适的值
选择一个 hash 函数,可以通过给 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 (i = 0; i < k; i++) // k是hash函数的个数Pos[i] = (hash1 + i*hash2) % m; // m是位图的⼤⼩
hash 函数实现过程当中为什么会出现 i * 31
分布式一致性hash是为了解决分布式缓存系统中的热点问题和数据迁移问题,他是将哈希空间组织成一个虚拟的圆环,圆环的大小是2322^{32}232
hash(ip)%232hash(ip) \% 2^{32}hash(ip)%232,最终会得到一个 [0,232][0,\ 2^{32}][0, 232] 之间的一个无符号整型,这个整数代表服务器的编号;多个服务器都通过这种方式在 hash 环上映射一个点来标识该服务器的位置;当用户操作某个 key,通过同样的算法生成一个值,沿环顺时针定位某个服务器,那么该 key 就在该服务器中
简单地说,分布式一致性hash是一种将数据均匀地映射到不同服务器的算法,它使用了一个虚拟的环形空间,将服务器和数据都哈希到这个空间上,然后按照顺时针方向找到最近的服务器作为存储或访问的目标
当服务器增加或减少时,只有与该服务器相邻的一小部分数据需要重新映射,而大部分数据仍然保持原来的映射关系,从而实现了一致性
应用场景主要是分布式缓存系统和分布式数据库系统,例如Redis集群,它们需要将大量的数据均匀地分配到不同的服务器上,并且在服务器增加或减少时,能够保证数据的可迁移和高可用;除了这些,一致性hash还可以应用于负载均衡、P2P网络、Web缓存等领域
hash偏移是指当服务器节点增加或减少时,原来的数据映射关系会发生变化,导致部分数据需要重新定位到新的服务器节点上
这种偏移会带来一些问题,例如请求访问不均匀、服务器承受的压力不均匀、缓存失效、数据迁移开销、负载不均等
为了解决这些问题,一致性hash引入了虚拟节点、负载因子、复制等技术,使得数据分布更加均匀和稳定
为了解决哈希偏移的问题,增加了虚拟节点的概念,理论上哈希环上节点数越多,数据分布越均衡
对每一个服务器节点计算多个哈希值,通常做法是:hash("IP:PORT:seqno")%232hash("IP:PORT:seqno")\%2^{32}hash("IP:PORT:seqno")%232,每个哈希值对应一个虚拟节点,然后将这些虚拟节点均匀地分布在哈希环上;这样,当需要存储或查找数据时,就可以根据数据的哈希值找到最近的虚拟节点,然后再找到该虚拟节点所属的服务器节点
虚拟节点的好处是可以增加服务器节点的负载均衡性,减少服务器节点增加或减少时的数据迁移量,提高系统的可扩展性和可用性
进行数据迁移的原理是,当服务器节点增加或减少时,只需要将原来映射到该节点的数据重新映射到哈希环上的下一个节点,而不需要重新计算所有数据的哈希值和映射关系;这样,可以大量减少数据迁移的量,提高系统的效率和稳定性
例如,假设系统中有三台服务器节点A、B、C,在哈希环上的位置分别为0、100、200。如果要增加一台服务器节点D,其位置为150,则只需要将原来映射到C的数据中哈希值在150~200之间的部分迁移到D即可,而不需要改变其他数据的映射关系
一个完整的例子:https://github.com/metang326/consistent_hashing_cpp