你知道快速的Redis有哪些慢操作吗?( 二 )


毕竟,哈希桶的个数通常要少于 key 的数量,这也就是说,难免会有一些 key 的哈希值对应到了同一个哈希桶中 。
Redis 解决哈希冲突的方式,就是链式哈希 。链式哈希也很容易理解,就是指同一个哈希桶中的多个元素用一个链表来保存 , 它们之间依次用指针连接 。
如下图所示:entry1、entry2 和 entry3 都需要保存在哈希桶 3 中,导致了哈希冲突 。此时,entry1 元素会通过一个*next指针指向 entry2,同样,entry2 也会通过*next指针指向 entry3 。这样一来,即使哈希桶 3 中的元素有 100 个,我们也可以通过 entry 元素中的指针 , 把它们连起来 。这就形成了一个链表,也叫作哈希冲突链 。

你知道快速的Redis有哪些慢操作吗?

文章插图
但是,这里依然存在一个问题,哈希冲突链上的元素只能通过指针逐一查找再操作 。如果哈希表里写入的数据越来越多 , 哈希冲突可能也会越来越多 , 这就会导致某些哈希冲突链过长,进而导致这个链上的元素查找耗时长,效率降低 。对于追求“快”的 Redis 来说,这是不太能接受的 。
所以,Redis 会对哈希表做 rehash 操作 。rehash 也就是增加现有的哈希桶数量 , 让逐渐增多的 entry 元素能在更多的桶之间分散保存,减少单个桶中的元素数量,从而减少单个桶中的冲突 。那具体怎么做呢?
其实 , 为了使 rehash 操作更高效,Redis 默认使用了两个全局哈希表:哈希表 1 和哈希表 2 。一开始,当你刚插入数据时,默认使用哈希表 1 , 此时的哈希表 2 并没有被分配空间 。随着数据逐步增多,Redis 开始执行 rehash,这个过程分为三步:
  • 给哈希表 2 分配更大的空间 , 例如是当前哈希表 1 大小的两倍;
  • 把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中;
  • 释放哈希表 1 的空间 。
到此 , 我们就可以从哈希表 1 切换到哈希表 2,用增大的哈希表 2 保存更多数据,而原来的哈希表 1 留作下一次 rehash 扩容备用 。
这个过程看似简单 , 但是第二步涉及大量的数据拷贝,如果一次性把哈希表 1 中的数据都迁移完,会造成 Redis 线程阻塞 , 无法服务其他请求 。此时,Redis 就无法快速访问数据了 。
为了避免这个问题,Redis 采用了渐进式 rehash 。
简单来说就是在第二步拷贝数据时 , Redis 仍然正常处理客户端请求,每处理一个请求时,从哈希表 1 中的第一个索引位置开始,顺带着将这个索引位置上的所有 entries 拷贝到哈希表 2 中;等处理下一个请求时,再顺带拷贝哈希表 1 中的下一个索引位置的 entries 。如下图所示:
你知道快速的Redis有哪些慢操作吗?

文章插图
渐进式rehash
这样就巧妙地把一次性大量拷贝的开销,分摊到了多次处理请求的过程中,避免了耗时操作,保证了数据的快速访问 。
好了 , 到这里,你应该就能理解,Redis 的键和值是怎么通过哈希表组织的了 。对于 String 类型来说 , 找到哈希桶就能直接增删改查了,所以,哈希表的 O(1) 操作复杂度也就是它的复杂度了 。
但是 , 对于集合类型来说,即使找到哈希桶了,还要在集合中再进一步操作 。接下来,我们来看集合类型的操作效率又是怎样的 。
集合数据操作效率和 String 类型不同,一个集合类型的值,第一步是通过全局哈希表找到对应的哈希桶位置 , 第二步是在集合中再增删改查 。那么,集合的操作效率和哪些因素相关呢?
首先,与集合的底层数据结构有关 。例如 , 使用哈希表实现的集合,要比使用链表实现的集合访问效率更高 。其次,操作效率和这些操作本身的执行特点有关,比如读写一个元素的操作要比读写所有元素的效率高 。
【你知道快速的Redis有哪些慢操作吗?】接下来,我们就分别聊聊集合类型的底层数据结构和操作复杂度 。
有哪些底层数据结构?刚才 , 我也和你介绍过,集合类型的底层数据结构主要有 5 种:整数数组、双向链表、哈希表、压缩列表和跳表 。
其中,哈希表的操作特点我们刚刚已经学过了;整数数组和双向链表也很常见,它们的操作特征都是顺序读写,也就是通过数组下标或者链表的指针逐个元素访问,操作复杂度基本是 O(N),操作效率比较低;压缩列表和跳表我们平时接触得可能不多,但它们也是 Redis 重要的数据结构,所以我来重点解释一下 。


推荐阅读