「这是我参与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。
哈希冲突
从图上我们还可以看到,需要写入 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