【Redis】八股总结三:数据结构

238 阅读7分钟

看小林coding 和 黑马redis

Redis 数据结构

常见问题

redis有哪些数据结构

sds简单动态字符串,(压缩列表)、哈希表、整数集合、跳表、(双向链表)

quicklist、listpack

😖Redis 为什么用跳表,而不用平衡树、红黑树

三点(内存占用,实现难度,范围查找操作,插入删除操作)

Redis 数据结构

Redis为什么用跳表实现有序集合

  1. 从内 存占用上来比较 ,跳表比平衡树更灵活一些。平衡树每个节点包含 2 个指针,跳表每个节点平均包含1.333个指针,在redis中。
  2. 在做 范围查找 的时候,跳表比平衡树操作要简单(比如Zset的zrangebyscore命令) 。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点,而跳表是基于链表结构的,找到最小值之后可以根据链表遍历。
  3. 从算法 实现难度上 和 维护上 来比较,跳表比平衡树要简单得多 并且 容易维护。(跳表的发明者在论文中说,插入删除比平衡树更高效)平衡树插入删除时 引发树的调整,而跳表只需要改变相邻节点的指针。

😖Redis 为什么用跳表,而不用B+树

两点

Redis为什么用跳表实现有序集合

B+树是多路平衡树。它的核心思想是通过可能少的 IO 定位到尽可能多的索引来获得查询数据。在MYSQL中可以大大减小树的高度。

  1. 但是Redis 这种内存数据库来说,它对这些并不感冒(减 少IO 存储大量数据) ,因为 Redis 作为内存数据库它不可能存储大量的数据。
  2. 而且 跳表的 实现和维护也比B+树 简单,不需要像B+树那样维护的时候有分裂合并的操作。

😖Redis 为什么使用 ListPack 替代 ZipList?

  1. listpack 中每个节点不再包含前一个节点的长度了,压缩列表每个节点正因为需要保存前一个节点的长度字段,就会有连锁更新的隐患。

那epoll网络模型和jdk1.8中解决哈希冲突,为什么用红黑树 不用跳表?

跳表的缺点有什么?

  1. 红黑树更稳定,跳表其实是不稳定的(跳表插入元素时元素层数是随机选的!!!!!!!!)
  2. 红黑树占用空间更少,跳表更占用空间(还不确定)

Redis中哈希表如何解决哈希冲突的?

链表、链地址法,根据dictEntry结构体可以判断。

ZipList压缩列表 的连锁更新问题?

  • 压缩列表的每个节点中,有个prelen字段记录前一个节点的长度。
  • 如果前一个节点的长度小于254,那么prelen为1字节,如果前一个节点长度大于等于254,那么prelen为5字节。
  • 这时如果压缩列表中很多节点长度都为250~253,如果改变其中一个节点长度使其大于等于254,那么就会引起后续节点的prelen连锁更新

说说redis的渐进式rehash?(字节)

hash扩容,创建一个新的数组,将原数组中的key重新计算hash值放入新数组中

过程

  1. 每次进行新增、删除、查找或者更新操作时,顺序将哈希表1某一个位置是的key value移动到哈希表2
  2. 扩容期间,新增元素往哈希表2中加,修改和查询元素在哈希表1和2中都找一遍。

sds简单动态字符串

5种sds类型,分别是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64

这 5 种类型的主要区别就在于,它们数据结构中的 len 和 alloc 成员变量的数据类型不同

intset整数集合

整数集合的升级操作,升级的时候倒序移动

连续内容,可以根据编码方式 进行数组的随机访问,例如访问第56个元素,根据编码方式快速寻址。

dict哈希表

两个dictht。rehash、渐进式rehash、rehash触发条件

typedef struct dictht {
    //哈希表数组
    dictEntry **table;
    //哈希表大小
    unsigned long size;  
    //哈希表大小掩码,用于计算索引值
    unsigned long sizemask;
    //该哈希表已有的节点数量
    unsigned long used;
} dictht;

渐进式rehash扩容

为了避免 rehash 在数据迁移过程中,因拷贝数据的耗时。

  • 在 rehash 进行期间,每次哈希表元素进行新增、删除、查找或者更新操作时,Redis 除了会执行对应的操作之外, 还会顺序将「哈希表 1 」中索 引位置上的所有 key-value 迁移到「哈希表 2」 上
  • 这样就巧妙地把一次性大量数据迁移工作的开销,分摊到了多次处理请求的过程中,避免了一次性 rehash 的耗时操作。

比如,查找一个 key 的值的话先会在「哈希表 1」 里面进行查找,如果没找到,就会继续到哈希表 2 里面进行找到

另外,在渐进式 rehash 进行期间,新增一个 key-value 时, 会被保存到「哈希表 2 」里面 ,而「哈希表 1」 则不再进行任何添加操作,这样保证了「哈希表 1 」的 key-value 数量只会减少,随着 rehash 操作的完成,最终「哈希表 1 」就会变成空表。

总结:

  1. 为了避免减少rehash的耗时,每次进行增删改操作时,顺序将哈希表1中某一个位置是的key value移动到哈希表2
  2. 扩容时,新增元素往哈希表2中加,修改和查询元素在哈希表1和2中都找一遍。

ziplist压缩列表

压缩列表在表头有三个字段:

  • zlbytes,记录整个压缩列表占用对内存字节数;
  • zltail,记录压缩列表「尾部」节点距离起始地址由多少字节,也就是列表尾的偏移量;
  • zllen,记录压缩列表包含的节点数量;
  • zlend,标记压缩列表的结束点,固定值 0xFF(十进制255)。

  • prevlen,记录了「前一个节点」的长度,目的是为了实现从后向前遍历;(如果前一个节点长度小于254,prelen占1字节,如果前一个节点长度大于等于254,prelen占5字节)
  • encoding,记录了当前节点实际数据的「类型和长度」,类型主要有两种:字符串和整数
  • data,记录了当前节点的实际数据,类型和长度都由 encoding ****决定;

ziplist连锁更新

跳表

skipList(跳表)首先是链表,但与传统链表相比有几点差异:

  1. 元素按照升序排列存储。
  2. 节点可能包含多个指针,指针跨度不同。

查找一个跳表节点的过程时,跳表会从头节点的最高层开始,逐一遍历每一层。

当数据量很大时,跳表的查找复杂度就是 O(logN)。

redis中的实现

listPack

简单了解

主要包含三个方面内容:

  • encoding,定义该元素的编码类型,会对不同长度的整数和字符串进行编码;
  • data,实际存放的数据;
  • len,encoding+data的总长度;

可以看到,listpack 没有压缩列表中记录前一个节点长度的字段了,listpack 只记录当前节点的长度,当我们向 listpack 加入一个新元素的时候,不会影响其他节点的长度字段的变化从而避免了压缩列表的连锁更新问题。