02-Redis 哈希表的门道

1,696 阅读5分钟

大家好,我是飓风。

大家都知道,redis 是一种key value 数据库,我可以直接通过get,hget等命令获得到相对应的value 值,那么你知道redis 内部是怎么根据key 找到 对应的value 的吗?查找的时间复杂度是多少呢?
今天我们就聊聊这个过程。

要想很快的获取到一个key 所对应的value ,作为一名java开发,我相信大多数人,很快就会想到HashMap 这个类,能够以o(1) 的复杂度定位到你想要的key,其实redis 也是这么做的,就是用到了redis内部的哈希表。数据类型hash 的底层数据结构。

上篇文章我们介绍了redis 的多种数据类型,每个类型在进行读写操作的时候,都基于一个key,要想根据这个key,能够快找到对应的数据类型的value,我们需要一个很大哈希表,来存储这个对应关系,这个哈希表我们成为全局哈希表。

全局哈希表

定义

redis 为了实现所有key、value的快速访问,redis 使用了一个哈希表来保存所有key、value 的对应关系。所谓的哈希表,就是一个很大的数组,数组内的每个元素,称为哈希桶。一个key 过来,我们只需计算这个key的hash值就可以定位到桶的位置(具体的hash散列算法这里不深入研究,计算完hash值,可以对桶的个数取模就得到数组index)。哈希桶内保存并不是值本身,而是 key 、value 的指针。key 就是String,value 就可以是任何redis 的数据类型了。 只管的看下图:

image.png

hash 冲突

什么是hash冲突呢,就是不同的key 最后计算出在数组上的位置相同,如果不做处理,那么就会覆盖之前的key,那么应该怎么解决这个问题?

java开发的小伙伴肯定都是HashMap 这个类是怎么解决hash key 冲突的,其实redis 也是大同小异,也采用了链表的方式来解决此问题,如果遇到相同位置的key,就会形成一个链表,也可以叫冲突链表,通过next 指针进行相连,看下图你就明白了。

image.png

这里有一个问题,如果hash key 冲突很高,那么有的桶的链表就会变的很长,那么查找一个key 所需要的时间就会增加,因为需要逐一遍历链表上的key进行k比较,这对于要求延迟低,速度快的redis来说是肯定不能容忍的,此时redis 会进行哈希表的重建,也就是rehash的过程。

rehash

什么是rehash ,说白了就是数组的大小太小了,元素的个数很多,那么最后肯定会存在一个桶内的链表过长,所以rehash 过程,简单点说就是要增加桶的数量,来更加均匀的分布这些键值对,进而缩短查找key 的时间。

那么redis 进行rehash 的过程是怎么样的呢?
如果是你,你会怎么进行呢?这个过程会不会阻塞redis 读写操作的主线程呢?
一次性全部完成,新的请求进来,如何处理,等待吗?显然这样做的是无法忍受的。

其实redis 并不是用主线进行一次性rehash的,下面我来详细的说明:

redis 哈希表在初始化的时候,会用到dict 字典这个结构,这个结构里有个哈希表的数组 dictht ht[2],一开始redis 使用 ht[0],随着键值对的增多,hash key 的冲突就会越多,链表就会边长,此时就要进行rehash:

  1. redis 分配给ht[1] 更大内存空间,可以是ht[0] 的两倍。
  2. copy ht[0] 的内的桶数据到 ht[1] 中。
  3. 切换到 ht[1],释放 ht[0], 等待下次rehash 使用。

在进行第二步骤的时候,redis 采用的是渐进式rehash ,来防止读写线程的阻塞。

什么是渐进式rehash呢?

所谓渐进式,其实就是redis 还是正常的处理客户端的读写请求,但是在处理客户端请求的时候,捎带着会进行一次桶的拷贝,也就是说客户端每来一次请求,就会进行一次桶的拷贝,注意这里,桶的copy 是从第一个位置开始,copy的是桶内的链表,然后进行重新分配到ht[1]上去。 看下图:

image.png

此外reids 会一个定时任务会周期性的去 copy ht[0] 的内的桶数据到 ht[1] 中,防止冷key 没有被copy。

在rehash过程中,redis 还是会处理请求的?那么redis 怎么查找这个key 呢?

redis 会先从 ht[0] 中查找,如果 ht[0] 有就直接返回,没有就去ht[1] 中查找。 更新和删除和查找一样的逻辑。
另外, 在渐进式 rehash 执行期间, 新添加到字典的键值对一律会被保存到 ht[1] 里面, 而 ht[0] 则不再进行任何添加操作: 这一措施保证了 ht[0] 包含的键值对数量会只减不增, 并随着 rehash 操作的执行而最终变成空表。

总结

今天学习了redis 是怎么查找一个key的,利用到了全局哈希表,全局哈希表就是一个数组,如果遇到key冲突,数组的元素就会形成链表,随着key的增多,桶的大小是一定的,那么key hash 冲突概率就会增大,此时链表的长度就会大大增加,会验证影响查询速度,此时就会进行rehash,redis 不会一次性copy 所有桶元素到新的哈希表内进行分配,而是采用渐进式rehash,来防止读写操作的阻塞。

另外还需要注意的是redis 内 采用哈希表为底层数据结构的类型 都会进行rehash。


今天的分享就到这里了,码字画图不易,期待你的点赞、关注、转发,谢谢。

欢迎关注 github

微信添加: zookeeper0