哈希
- 1.哈希表为什么快?
- 2.哈希冲突解决方法
- 3.哈希表扩容流程
- 4.哈希表扩容太多次,需要遍历所有元素,如何优化?
- 5.渐进式扩容为何可以正确访问哈希表?
1.哈希表为什么快?
哈希表
(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。相对于传统的线性查找,需要查找一个数组的元素,需要遍历整个数组,如果存在就返回.
而通过hash表,就能够大幅度的提高查找的效率.
2.哈希冲突解决方法
链式解决法
:将所有哈希地址相同的记录都链接在同一链表中。这样,即使多个数据项具有相同的哈希值,它们也会被存储在同一个链表中,从而解决了冲突。当需要查找某个数据时,首先通过哈希函数计算出其哈希地址,然后在对应的链表中查找即可。比方说当20经过映射后已经存放到了table的0号位置, 则当60进来时,我们只需要设置一个指针让40指向60链式的去存储,就可以去避免冲突.
开放地址------线性勘测法
:如果遇到冲突就往下一个位置寻找空位.遇到冲突,新位置=原始位置+i(i是冲突的次数)
假设数字关键字有 15 2 38 28 4 12 数组大小为13 下标=关键字%数组大小
则就会变成如下顺序去存储
开放地址------平方勘测法
:线性勘测法会让数据可能会让数据扎堆,而平方勘测法就能够去解决这个问题。如果遇到冲突就往下一个位置寻找空位. 则遇到冲突步长就会变成平方 新位置=原始位置+i^2(i是冲突的次数)
数据关键字:15 2 28 19 10 数组大小:13 下标=关键字%数组大小
则就会变成如下顺序去存储
再哈希法
:故名思意,就是再开一个hash函数
R要取比数组尺寸小的质数。比如数组尺寸为13则可
R=7: hash2(关键字) = 7-(关键字%7)
也就是说,二次哈希的结果在1-7之间,不会等于0;
如果遇到冲突, 新位置= 原始位置+ i · hash 2(关键字)
数据关键字:15 2 18 28
数组大小: 13
哈希函数: 下标= 关键字 mod 13
哈希函数2: 7-(关键字%7)
如果遇到冲突新位置=原始+ i . hash 2(关键字)
旧表: 下标=关键字%7
新表: 下标=关键字%17
3.哈希表扩容流程
为了解决hash退化,引入了两个概念:
负载因子(load_factor)
,是hashtable的元素个数与hashtable的桶数之间比值;
最大负载因子(max_load_factor)
,是负载因子的上限
他们之间要满足:
load_factor = map.size() / map.buck_count() // load_factor 计算方式
load_factor <= max_load_factor // 限制条件
当hashtable中的元素个数与桶数比值load_factor >= max_load_factor
时,hashtable就自动发生Rehash
行为,来降低load_factor
。
哈希表hash的扩容
随着操作的不断执行, 哈希表保存的键值对会逐渐地增多或者减少, 为了让哈希表的负载因子(load factor)维持在一个合理的范围之内, 当哈希表保存的键值对数量太多或者太少时, 程序需要对哈希表的大小进行相应的扩展或者收缩。
扩展和收缩哈希表的工作可以通过执行 rehash (重新散列)操作来完成, Redis 对字典的哈希表执行 rehash 的步骤如下:
- 为字典的 ht[1] 哈希表分配空间, 这个哈希表的空间大小取决于要执行的操作以及 ht[0] 当前包含的键值对数量(也即是ht[0].used 属性的值):如果执行的是扩展操作, 那么 ht[1] 的大小为第一个大于等于ht[0].used * 2 的 2^n (2 的 n 次方幂);如果执行的是收缩操作, 那么 ht[1] 的大小为第一个大于等于
ht[0].used 的2^n 。 - 将保存在 ht[0] 中的所有键值对 rehash 到 ht[1] 上面: rehash指的是重新计算键的哈希值和索引值,然后将键值对放置到 ht[1] 哈希表的指定位置上。
- 当 ht[0] 包含的所有键值对都迁移到了 ht[1] 之后 (ht[0] 变为空表), 释放 ht[0] , 将 ht[1] 设置为ht[0] , 并在 ht[1] 新创建一个空白哈希表, 为下一次 rehash 做准备。
4.哈希表扩容太多次,需要遍历所有元素,如何优化?
1.渐进式哈希
可以采用渐进式哈希,扩展或收缩哈希表需要将 ht[0] 里面的所有键值对 rehash 到 ht[1] 里面, 但是不是一次性完成所有元素的迁移,可以在插入、查找等操作中逐步迁移数据。这样可以将扩容的开销分散到多个操作中,减少对系统性能的影响。
这样做的原因在于, 如果 ht[0] 里只保存着四个键值对, 那么服务器可以在瞬间就将这些键值对全部 rehash 到 ht[1] ; 但是, 如果哈希表里保存的键值对数量不是四个, 而是四百万、四千万甚至四亿个键值对, 那么要一次性将这些键值对全部 rehash 到 ht[1] 的话, 庞大的计算量可能会导致服务器在一段时间内停止服务。
因此, 为了避免 rehash 对服务器性能造成影响, 服务器不是一次性将 ht[0] 里面的所有键值对全部 rehash 到 ht[1] , 而是分多次、渐进式地将 ht[0] 里面的键值对慢慢地 rehash 到 ht[1] 。
以下是哈希表渐进式 rehash的详细步骤:
- 为 ht[1] 分配空间, 让字典同时持有 ht[0] 和 ht[1] 两个哈希表。 在字典中维持一个索引计数器变量 rehashidx,并将它的值设置为 0 , 表示 rehash 工作正式开始。
- 在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0]哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将rehashidx 属性的值增一。
- 随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时程序将rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。
渐进式 rehash 的好处在于它采取分而治之的方式, 将 rehash 键值对所需的计算工作均滩到对字典的每个添加、删除、查找和更新操作上, 从而避免了集中式 rehash 而带来的庞大计算量。
2.链表节点
hashMap需要遍历原数组中的所有元素。为了提高性能,HashMap会采取一些优化措施。例如,它会将原数组中的元素分成多个链表,每个链表称为一个链表节点(Entry)。这样,在进行重新哈希时,只需遍历每个链表节点,而不需要遍历整个数组。这种方式可以减少遍历的次数,提高扩容的效率。
5.渐进式扩容为何可以正确访问哈希表?
渐进式哈希的精髓在于:数据的迁移不是一次性完成的,而是可以通过dictRehash()这个函数分步规划的,并且调用方可以及时知道是否需要继续进行渐进式哈希操作。如果dict数据结构中存储了海量的数据,那么一次性迁移势必带来redis性能的下降,别忘了redis是单线程模型,在实时性要求高的场景下这可能是致命的。而渐进式哈希则将这种代价可控地分摊了,调用方可以在dict做插入,删除,更新的时候执行dictRehash(),最小化数据迁移的代价。
在迁移的过程中,系统会同时维护旧表和新表。当进行数据访问时,系统首先会在新表中查找所需的数据。如果新表中不存在该数据,系统则会回退到旧表中查找。这种双重查找机制确保了无论是在扩容过程中,还是扩容完成后,数据都可以被正确地访问到。
谈Redis的refash的增量式扩容
redis 哈希表的 rehash 分析