Redis系列之底层数据结构具体实现(Hash表)

867 阅读3分钟

「这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战

Redis的Hash类型的底层实现有压缩列表和Hash表两个数据结构。

压缩列表的实现请看:压缩列表实现

Hash表除了用作Redis的一个类型外,同时Redis也是用Hash表维护着全局的键值对。

今天说一下Hash表的具体实现。

Hash表

Hash表的应用非常广泛,最主要的原因就是能够以O(1)的复杂度快速查询。那么是如何实现的呢?

一个最简单的 Hash 表就是一个数组,数组里的每个元素是一个哈希桶(也叫做Bucket),第一个数组元素被编为哈希桶 0,以此类推。当一个键值对的键经过 Hash 函数计算后,再对数组元素个数取模,就能得到该键值对对应的数组元素位置,也就是第几个哈希桶。

如下图,key1 经过哈希计算和哈希值取模后,就对应哈希桶 1,类似的,key3 和key16 分别对应哈希桶 7 和桶 4。

Snipaste_2021-11-05_22-08-24.png

哈希冲突

从图上我们还可以看到,需要写入 Hash 表的键空间一共有 16 个键,而 Hash 表的空间大小只有 8 个元素,这样就会导致有些键会对应到相同的哈希桶中。这种情况就是哈希冲突。

为了解决哈希冲突,Redis采用链式哈希的方法不同的键对应到相同的哈希桶中。

当有相同的键需要插入时,在哈希桶中,就会行成一个链表,链表的节点上记录的就是每个键的值。当查询一个键时,如果对用的哈希桶中存储的是一个链表,就会再次根据键值找到对用的哈希项,这样就避免了哈希冲突。

采用链式哈希解决哈希冲突有一个问题,根据链表的结构,查询非链表头或链表尾的数据复杂度比较高,如果链表太长,会导致查询变慢,因此同一个哈希桶内的链表长度,需要控制。

如果哈希桶内的链表太长怎么处理呢?Redis采用rehash的方式解决,直白一点就是进行扩容。

rehash

Redis准备了两张表h1和h2,用于rehash时交替保存数据。

正常请求的时候,数据写入h1内,当进行rehash时,数据被迁移到h2内。迁移完成后,h1会被释放空间,并把h2的地址复制给h1,然后把h2的大小清空,这样就能继续使用h1存储数据。

执行rehash时,扩容的大小为当前已用表空间的2倍。

由于执行rehash时,迁移大量数据会造成主线程阻塞,对性能有影响,Redis使用渐进式rehash的方法避免这种情况。

Redis 并不会一次性的把所有的数据都进行迁移,而是每执行一次请求,就会顺带迁移部分数据,因为每次迁移的数据量很小,对主线程造成的影响也比较小,直到把所有数据都迁移完毕。

End