Redis 底层数据结构之哈希

181 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第13天,点击查看活动详情

键和值的结构

Redis采用哈希表来保存所有键值对,从而实现用 O(1) 的时间复杂度来快速查找到键值对:计算键的哈希值,就可以知道它所对应的哈希桶的位置,然后就可以访问相应的 entry 元素,最后通过 entry 的指针访问到对应的值

一个哈希表,其实就是一个数组,数组的每个元素称为哈希桶,所以说一个哈希表由多个哈希桶组成,每个哈希桶保存了键值对数据

哈希桶保存的是指向具体值的指针,不管是String还是集合类型,哈希桶的元素都是指向他们的指针。所以即使值是一个集合,也可以通过 *value 指针查到对应的集合

1.png

  • redisDB中存放着指向dict的指针
  • dict存放两个哈希表,一个用于存放一个用于rehash
  • ditctht结构用于存放哈希表数组,数组中的每个元素都是指向一个哈希表节点结构(dictEntry)的指针;
  • dictEntry 结构,表示哈希表节点的结构,结构里存放了 void * key 和 void * value 指针, *key 指向的是 String 对象,而 *value 则可以指向 String 对象,也可以指向集合类型的对象,比如 List 对象、Hash 对象、Set 对象和 Zset 对象。

2.png 原理:计算键的哈希值,然后就能知道对应哈希桶的位置,然后访问相应的entry元素

这个查找过分依赖哈希计算,和数据量的多少没有直接关系。

这里的key和value都是redis对象,redis对象都由RedisObject结构表示:

  • type,标识该对象是什么类型的对象(String 对象、 List 对象、Hash 对象、Set 对象和 Zset 对象);
  • encoding,标识该对象使用了哪种底层的数据结构;
  • ptr,指向底层数据结构的指针。

3.png

问题

大量写入操作的时候,会变慢,因为可能出现哈希表的冲突问题和 rehash 可能带来的操作阻塞

如果大量数据写入的时候,由于桶的数量少于key的数量,难免一些key的哈希值对应到同一个桶中

Redis采用链式哈希解决哈希冲突:同一个哈希桶中的多个元素用一个链表来保存,它们之间依次用指针连接

但依然会存在问题:如果一个桶中链表元素过多(链表过长),就要逐一查找再操作,查询效率就会变低。Redis会进行rehash操作。也就是增加现有哈希桶的数量,让逐渐增多的entry元素能在更多的桶之间分散保存,减少单个桶中的元素数量,从而减少单个桶中的冲突

做法:使用两个全局哈希表,哈希表1和哈希表2,一开始,当你刚插入数据时,默认使用哈希表 1,此时的哈希表 2 并没有被分配空间。随着数据逐步增多,Redis 开始执行 rehash:

  1. 给哈希表2分配更大的空间,例如哈希表1的2倍
  2. 把哈希表1中的元素重新映射并拷贝到哈希表2中
  3. 释放哈希表1

为避免一次性将数据迁移导致的Redis线程阻塞,无法服务其他请求,此时,redis无法快速访问数据了,因此redis采用渐进式rehash:每次处理一个请求时,就从哈希表1中的第一个索引位置开始,顺带着将这个索引位置上的所有 entries 拷贝到哈希表 2 中;等处理下一个请求时,再顺带拷贝哈希表 1 中的下一个索引位置的entries。

4.png 3.png

把一次性大量拷贝的开销,分摊到了多次处理请求的过程中,避免了耗时操作,保证了数据的快速访问。

rehash触发条件

  • 当负载因子大于等于 1 ,并且 Redis 没有在执行 bgsave 命令或者 bgrewiteaof 命令,也就是没有执行 RDB 快照或没有进行 AOF 重写的时候,就会进行 rehash 操作。
  • 当负载因子大于等于 5 时,此时说明哈希冲突非常严重了,不管有没有有在执行 RDB 快照或 AOF 重写,都会强制进行 rehash 操作。

为什么Redis的负载因子可以大于1,而HashMap的负载因子小于等于1?因为redis的负载因子的计算公式是键值对数量除以长度,而HashMap是已使用位置的数量除以长度