上一篇笔记讲的是 Redis 里字典的实现,这一篇在上一篇基础上解释这个字典能干什么。
如何解决散列冲突
链表法
当有两个或以上的键被分配到散列表数组同一个索引上时,就发生了键冲突。Redis 使用链表法解决散列冲突。每个散列表节点都有一个 next 指针,多个散列表节点可以用 next 指针构成一个单向链表,被分配到同一个索引上的多个节点可以使用这个单向链表连接起来。上一篇笔记里就有这样的图,这次不贴了。
rehash
随着操作的进行,散列表中保存的键值对会也会不断地增加或减少,为了保证负载因子维持在一个合理的范围,当散列表内的键值对过多或过少时,内需要定期进行 rehash,以提升性能或节省内存。Redis 的 rehash 的步骤如下:
- 为字典的
ht[1]散列表分配空间,这个空间的大小取决于要执行的操作以及ht[0]当前包含的键值对数量(即ht[0].used的属性值)
-
扩展操作:
ht[1]的大小为第一个大于等于ht[0].used * 2的 2 的 n 次方幂。举个例子ht[0].used = 3则ht[1]的大小为 8,ht[0].used = 4则ht[1]的大小为 8。 -
收缩操作:
ht[1]的大小为第一个大于等于ht[0].used的 2 的 n 次方幂。
- 将保存在
ht[0]中的键值对重新计算键的散列值和索引值,然后放到ht[1]指定的位置上。
- 将
ht[0]包含的所有键值对都迁移到了ht[1]之后,释放ht[0],将ht[1]设置为ht[0],并创建一个新的ht[1]哈希表为下一次 rehash 做准备。
rehash 的条件
- 服务器目前没有执行 BGSAVE(rdb 持久化)命令或者 BGREWRITEAOF(AOF 文件重写)命令,并且散列表的负载因子大于等于 1。
- 服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令,并且负载因子大于等于 5。
- 当负载因子小于 0.1 时,程序自动开始执行收缩操作。
Redis 这么做的目的是基于操作系统创建子进程后写时复制技术,避免不必要的写入操作。
渐进式 rehash
对于 rehash 我们思考一个问题:如果散列表当前大小为 1 GB,要想扩容为原来的两倍大小,那就需要对 1 GB 的数据重新计算哈希值,并且从原来的散列表搬移到新的散列表。为了解决一次性扩容耗时过多的情况,可以将扩容操作穿插在插入操作的过程中,分批完成。当负载因子触达阈值之后,只申请新空间,但并不将老的数据搬移到新散列表中。当有新数据要插入时,将新数据插入新散列表中,并且从老的散列表中拿出一个数据放入到新散列表。每次插入一个数据到散列表,都重复上面的过程。经过多次插入操作之后,老的散列表中的数据就一点一点全部搬移到新散列表中了。这样没有了集中的一次一次性数据搬移,插入操作就都变得很快了。
Redis为了解决这个问题采用渐进式 rehash 方式。以下是 Redis 渐进式 rehash 的详细步骤:
- 为
ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表。 - 在字典中维持一个索引计数器变量
rehashidx,并将它的值设置为0,表示 rehash 工作正式开始。 - 在 rehash 进行期间,每次对字典执行添加,删除,查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将
ht[0]哈希表在rehashidx索引上的所有键值对 rehash 到ht[1],当 rehash 工作完成之后,程序将rehashidx属性的值增一。 - 随着字典操作的不断执行,最终在某个时间点上,
ht[0]的所有键值对都会被 rehash 至ht[1],这时程序将rehashidx属性的值设为-1,表示 rehash 操作已完成。
在进行渐进式 rehash 的过程中,两个哈希表里的内容是一点一点转移过去的,增删改查里的删改查三个操作在两个哈希表上会多次重复。在渐进式 rehash 执行期间,新添加到字典的键值对一律会被保存到 ht[1] 里面,而 ht[0] 则不再进行任何添加操作。这一措施保证了 ht[0] 包含的键值对数量会只减不增,并随着 rehash 操作的执行而最终变成空表。
常用 API
| 函数 | 作用 | 时间复杂度 |
|---|---|---|
dictCreate | 创建新的字典 | |
dictAdd | 将给定的键值对添加到字典里 | |
dictReplace | 将给定的键值对添加到字典里,如果字典中已经存在这个键,用新的值替换之前的值 | |
dictFetchValue | 返回给定键的值 | |
dictGetRandomKey | 从字典中随即返回一个键值对 | |
dictDelete | 从字典中删除给定键所对应的键值对 | |
dictRelease | 释放给定字典,以及字典中包含的所有键值对 | , 是字典里键值对数量 |