Redis 字典相关操作 | 青训营

59 阅读5分钟

上一篇笔记讲的是 Redis 里字典的实现,这一篇在上一篇基础上解释这个字典能干什么。

如何解决散列冲突

链表法

当有两个或以上的键被分配到散列表数组同一个索引上时,就发生了键冲突。Redis 使用链表法解决散列冲突。每个散列表节点都有一个 next 指针,多个散列表节点可以用 next 指针构成一个单向链表,被分配到同一个索引上的多个节点可以使用这个单向链表连接起来。上一篇笔记里就有这样的图,这次不贴了。

rehash

随着操作的进行,散列表中保存的键值对会也会不断地增加或减少,为了保证负载因子维持在一个合理的范围,当散列表内的键值对过多或过少时,内需要定期进行 rehash,以提升性能或节省内存。Redis 的 rehash 的步骤如下:

  1. 为字典的 ht[1] 散列表分配空间,这个空间的大小取决于要执行的操作以及 ht[0] 当前包含的键值对数量(即 ht[0].used 的属性值)
  • 扩展操作:ht[1] 的大小为第一个大于等于 ht[0].used * 2 的 2 的 n 次方幂。举个例子 ht[0].used = 3ht[1] 的大小为 8,ht[0].used = 4ht[1] 的大小为 8。

  • 收缩操作:ht[1] 的大小为第一个大于等于 ht[0].used 的 2 的 n 次方幂。

  1. 将保存在 ht[0] 中的键值对重新计算键的散列值和索引值,然后放到 ht[1] 指定的位置上。

  1. ht[0] 包含的所有键值对都迁移到了 ht[1] 之后,释放 ht[0],将 ht[1] 设置为 ht[0],并创建一个新的 ht[1] 哈希表为下一次 rehash 做准备。

rehash 的条件

  1. 服务器目前没有执行 BGSAVE(rdb 持久化)命令或者 BGREWRITEAOF(AOF 文件重写)命令,并且散列表的负载因子大于等于 1。
  2. 服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令,并且负载因子大于等于 5。
  3. 当负载因子小于 0.1 时,程序自动开始执行收缩操作。

Redis 这么做的目的是基于操作系统创建子进程后写时复制技术,避免不必要的写入操作。

渐进式 rehash

对于 rehash 我们思考一个问题:如果散列表当前大小为 1 GB,要想扩容为原来的两倍大小,那就需要对 1 GB 的数据重新计算哈希值,并且从原来的散列表搬移到新的散列表。为了解决一次性扩容耗时过多的情况,可以将扩容操作穿插在插入操作的过程中,分批完成。当负载因子触达阈值之后,只申请新空间,但并不将老的数据搬移到新散列表中。当有新数据要插入时,将新数据插入新散列表中,并且从老的散列表中拿出一个数据放入到新散列表。每次插入一个数据到散列表,都重复上面的过程。经过多次插入操作之后,老的散列表中的数据就一点一点全部搬移到新散列表中了。这样没有了集中的一次一次性数据搬移,插入操作就都变得很快了。

Redis为了解决这个问题采用渐进式 rehash 方式。以下是 Redis 渐进式 rehash 的详细步骤:

  1. 为 ht[1] 分配空间,让字典同时持有 ht[0] 和 ht[1] 两个哈希表。
  2. 在字典中维持一个索引计数器变量 rehashidx,并将它的值设置为 0,表示 rehash 工作正式开始。
  3. 在 rehash 进行期间,每次对字典执行添加,删除,查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1],当 rehash 工作完成之后,程序将 rehashidx 属性的值增一。
  4. 随着字典操作的不断执行,最终在某个时间点上,ht[0] 的所有键值对都会被 rehash 至 ht[1],这时程序将 rehashidx 属性的值设为 -1,表示 rehash 操作已完成。

在进行渐进式 rehash 的过程中,两个哈希表里的内容是一点一点转移过去的,增删改查里的删改查三个操作在两个哈希表上会多次重复。在渐进式 rehash 执行期间,新添加到字典的键值对一律会被保存到 ht[1] 里面,而 ht[0] 则不再进行任何添加操作。这一措施保证了 ht[0] 包含的键值对数量会只减不增,并随着 rehash 操作的执行而最终变成空表。

常用 API

函数作用时间复杂度
dictCreate创建新的字典O(1)O(1)
dictAdd将给定的键值对添加到字典里O(1)O(1)
dictReplace将给定的键值对添加到字典里,如果字典中已经存在这个键,用新的值替换之前的值O(1)O(1)
dictFetchValue返回给定键的值O(1)O(1)
dictGetRandomKey从字典中随即返回一个键值对O(1)O(1)
dictDelete从字典中删除给定键所对应的键值对O(1)O(1)
dictRelease释放给定字典,以及字典中包含的所有键值对O(N)O(N)NN 是字典里键值对数量