Redis高级(十二)、彻底掌握Redis基本数据类型及底层实现【下篇】IntSet、SkipList

52 阅读7分钟

觉得对你有益的小伙伴记得点个赞+关注

后续完整内容持续更新中

希望一起交流的欢迎发邮件至javalyhn@163.com

1. Set数据结构介绍

1.1 两个重要配置

set-max-intset-entrties (默认为512

image.png

1.2 set底层数据结构是只有intset吗

回答是否定的。

Redis用intset或hashtable存储set。如果元素都是整数类型,就用intset存储。 如果不是整数类型,就用hashtable(数组+链表的存来储结构)。key就是元素的值,value为null

image.png

1.3 IntSet源码解析

IntSet是Redis中set集合的一种实现方式,基于整数数组来实现,并且具备长度可变、有序等特征。 结构如下: image.png

其中的encoding包含三种模式,表示存储的整数大小不同:

image.png

为了方便查找,Redis会将intset中所有的整数按照升序依次保存在contents数组中,结构如图:

image.png

现在,数组中每个数字都在int16_t的范围内,因此采用的编码方式是INTSET_ENC_INT16,每部分占用的字节大小为:

  • encoding:4字节
  • length:2字节
  • contents:2字节 * 3 = 6字节

我们向该其中添加一个数字:50000,这个数字超出了int16_t的范围,intset会自动升级编码方式到合适的大小。

以当前案例来说流程如下:

  • 升级编码为INTSET_ENC_INT32, 每个整数占4字节,并按照新的编码方式及元素个数扩容数组
  • 倒序依次将数组中的元素拷贝到扩容后的正确位置
  • 将待添加的元素放入数组末尾
  • 最后,将inset的encoding属性改为INTSET_ENC_INT32,将length属性改为4

image.png

源码如下

image.png

image.png

1.4 IntSet总结

Intset可以看做是特殊的整数数组,具备一些特点:

  • Redis会确保Intset中的元素唯一、有序
  • 具备类型升级机制,可以节省内存空间
  • 底层采用二分查找方式来查询

2. ZSet数据结构介绍

2.1 两个重要配置

zset-max-ziplist-entrties (默认128)
zset-max-ziplist-value (默认64

image.png

2.2 zset底层只会使用skiplist吗

回答是否定的

zset底层采用 skiplist 与 ziplist

当有序集合中包含的元素数量超过服务器属性 server.zset_max_ziplist_entries 的值(默认值为 128 ), 或者有序集合中新添加元素的 member 的长度大于服务器属性 server.zset_max_ziplist_value 的值(默认值为 64 )时,redis会使用跳跃表作为有序集合的底层实现。否则会使用ziplist作为有序集合的底层实现。

在此我们减小配置大小用来演示

image.png

image.png

2.3 说说链表和数组的优缺点?为什么引出跳表

image.png

解决方案也就是升维(空间换时间)

image.png

image.png

2.4 skiplist

SkipList(跳表)首先是链表,但与传统链表相比有几点差异: 元素按照升序排列存储 节点可能包含多个指针,指针跨度不同。

skiplist是一种以空间换取时间的结构。由于链表,无法进行二分查找,因此借鉴数据库索引的思想,提取出链表中关键节点(索引),先在关键节点上查找,再进入下层链表查找。

总结来讲 跳表 = 链表 + 多级索引

image.png

image.png

2.5 skiplist时间复杂度分析

跳表查询的时间复杂度分析

首先每一级索引我们提升了2倍的跨度,那就是减少了2倍的步数,所以是n/2、n/4、n/8以此类推;

第 k 级索引结点的个数就是 n/(2^k);

假设索引有 h 级, 最高的索引有2个结点;n/(2^h) = 2

从这个公式我们可以求得 h = log2(N)-1

所以最后得出跳表的时间复杂度是O(logN)

2.6 skiplist空间复杂度分析

跳表查询的空间复杂度分析

首先原始链表长度为n

如果索引是每2个结点有一个索引结点,每层索引的结点数:n/2, n/4, n/8 ... , 8, 4, 2 以此类推;

或者索引是每3个结点有一个索引结点,每层索引的结点数:n/3, n/9, n/27 ... , 9, 3, 1 以此类推;

所以空间复杂度是O(n)

2.7 skiplist小总结

  • 跳跃表是一个双向链表,每个节点都包含score和ele值
  • 节点按照score值排序,score值一样则按照ele字典排序
  • 每个节点都可以包含多层指针,层数是1到32之间的随机数
  • 不同层指针到下一个节点的跨度不同,层级越高,跨度越大
  • 增删改查效率与红黑树基本一致,实现却更简单

跳表是一个最典型的空间换时间解决方案,而且只有在数据量较大的情况下才能体现出来优势。而且应该是读多写少的情况下才能使用,所以它的适用范围应该还是比较有限

维护成本相对要高 - 新增或者删除时需要把所有索引都更新一遍

最后在新增和删除的过程中的更新,时间复杂度也是O(log n)

3. 终章总结

3.1 redis数据类型以及数据结构的关系

image.png image.png

3.2 不同数据类型对应的底层数据结构

  1. 字符串

int:8个字节的长整型。

embstr:小于等于44个字节的字符串。

raw:大于44个字节的字符串。

Redis会根据当前值的类型和长度决定使用哪种内部编码实现。

  1. 哈希

ziplist(压缩列表):

当哈希类型元素个数小于hash-max-ziplist-entries 配置(默认512个)、同时所有值都小于hash-max-ziplist-value配置(默认64 字节)时,Redis会使用ziplist作为哈希的内部实现,ziplist使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比hashtable更加优秀。

hashtable(哈希表):

当哈希类型无法满足ziplist的条件时,Redis会使用hashtable作为哈希的内部实现,因为此时ziplist的读写效率会下降,而hashtable的读写时间复杂度为O(1)。

  1. 列表

ziplist(压缩列表):

当列表的元素个数小于list-max-ziplist-entries配置 (默认512个),同时列表中每个元素的值都小于list-max-ziplist-value配置时 (默认64字节),Redis会选用ziplist来作为列表的内部实现来减少内存的使用。

linkedlist(链表):

当列表类型无法满足ziplist的条件时,Redis会使用 linkedlist作为列表的内部实现。quicklist ziplist和linkedlist的结合以ziplist为节点的链表(linkedlist)

  1. 集合

intset(整数集合):

当集合中的元素都是整数且元素个数小于set-max-intset-entries配置(默认512个)时,Redis会选用intset来作为集合的内部实现,从而减少内存的使用。

hashtable(哈希表):

当集合类型无法满足intset的条件时,Redis会使用hashtable作为集合的内部实现。

  1. 有序集合

ziplist(压缩列表):

当有序集合的元素个数小于zset-max-ziplist- entries配置(默认128个),同时每个元素的值都小于zset-max-ziplist-value配 置(默认64字节)时,Redis会用ziplist来作为有序集合的内部实现,ziplist可以有效减少内存的使用。

skiplist(跳跃表):

当ziplist条件不满足时,有序集合会使用skiplist作 为内部实现,因为此时ziplist的读写效率会下降。

至此,有关Redis经典五种数据类型及底层实现就讲完了,如歌对各位小伙伴有帮助,不妨赞关注一下!