如大家众所周知,redis有string、list、hash、set、zset五种数据类型,但是大家对于每种数据类型的底层存储数据结构,可能还不是很清楚,在下面这篇文章中,主要讲述一下redis底层存储的7中数据类型。
1、简单动态字符串(SDS)(摘自redis设计与实现第二章)
由于C语言字符串长度的不可修改性,redis实现了一种可变长度的字符串,即SDS,SDS的实现原理如下
(1)len
存储了字符串的长度
由于SDS中存储了字符串,将获取字符串长度的时间复杂度从O(n)降到了O(1)
(2)free
存储了剩余可用的字符串长度
(3)buf
用于保存字符串
减少内存分配次数
(1)空间预分配:当用户修改字符串时,SDS API首先会检查空间是否满足需求,如果满足,则直接使用未使用的空间;如果不满足,则将空间扩展需要修改的内容大小,然后才执行修改操作
空间预分配:修改后SDS小于1MB, 预留free=len;修改后SDS大于1MB,预留free=1MB;通过空间预分配操作,redis有效的减少了执行字符串增长所需要的内存分配次数。
(2)惰性空间释放:当缩短SDS的存储内容时,并不会立即使用内存重分配来回收缩短操作后多出来的字节,而是用free将这些空间字节数量记录起来,并等待将来使用
当需要真正释放内存时,调用API真正的释放内存。
总结:
(1)redis只会使用C字符串作为字面使用量,在大多数情况下,redis使用SDS(Simple Dynamic String,简单动态字符串)作为字符串标识。
(2)比起C字符串,SDS具有一下优点:O(1)获取字符串长度;杜绝缓存区溢出;减少修改字符串长度,所需要的内存重分配次数;二进制安全;兼容部分C字符串函数
2、链表(摘自redis设计与实现第三章)
每一个链表节点都是使用listNode实现,其中prev指向前几点,next指向后节点。
在listNode基础上,redis又封装了一层list,使得链表的使用更加的高效。
其中head保存了链表头结点,tail保存了链表尾节点,len保存了链表的节点数。dup、free和match实现多态链表所需的类型特定函数
- dup 函数用于复制链表节点所保存的值
- free 函数用于释放链表节点所保存的值
- match 函数用于链表中保存的值,是否与输入的一个值是否相等。
下图为list和listNode组成的链表
redis实现链表的特性可以总结如下
- 双端:链表节点带有prev和next指针,获取某个节点的前置节点和后置节点的时间复杂度都是O(1)
- 无环:表头节点和prev和表尾节点的next都指向null,对链表的访问以NULL为终点
- 带表头指针和表尾指针:通过list结构的head和tail指针,使得获取头结点和尾节点的时间复杂度为O(1)
- 带链表长度计数器:对链表节点数进行计数,使得获取链表节点数的时间复杂度为O(1)
- 多态:链表节点使用void*指针来保存节点值,并使用list结构的dup、free、match三个属性为几点设置类型特定函数,所以链表可以保存各种不同类型的值。
3、字典
redis的字典使用哈希表作为底层实现,一个哈希表里可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对。一下主要介绍哈希表、哈希表节点、字典。
哈希表
其中:
- table是一个数组,其中一个数组元素都是一个指向哈希表节点的指针,每一个哈希表节点都保存了一对键值对。
- size记录了table的大小,也即是哈希表的大小
- used记录了当前已使用节点的数量
- sizemask=size-1 ,决定了一个键应该放tabl数组的哪个索引上
哈希表节点
结构定义如下图所示:
其中:
- key保存键值对中的键,
- v保存这键值对中的值
- next为指向另一个哈希表几点的指针
字典
定义如下图所示:
其中:
- type标识dictType类型,privatedata是指向特定数据类型的数组;
- ht包含了两个哈希表的数组,其中一个ht[0]保存哈希表,ht[1]只会在对ht[0]进行rehash时使用
字典的整体实现如下图所示:
字典还包括rehash和渐进式hash等要点,再这里就不讲述了
总结
- 字典被广泛应用于redis的各种功能,其中包括数据库和哈希键。
- redis中的字典使用哈希表作为底层实现,每个字典带有两个哈希表,一个平时使用,一个在rehash时使用。
- 当字典被用作数据库底层实现,或者哈希表底层实现时,redis使用muimuihash2算法计算哈希键的哈希值。
- 哈希表使用链地址法解决键冲突,被分配到同一个索引上的多个键值对会连接成一个单项链表
- rehash渐进式进程,并rehash到新的hash表中
4、跳跃表(待补充)
Redis使用跳跃表作为有序集合键的底层实现之一,如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员是比较长的字符串时, Redis就会使用跳跃表来作为有序集合健的底层实现。
这里我们需要思考一个问题——为什么元素数量比较多或者成员是比较长的字符串的时候Redis要使用跳跃表来实现?
从上面我们可以知道,跳跃表在链表的基础上增加了多级索引以提升查找的效率,但其是一个空间换时间的方案,必然会带来一个问题——索引是占内存的。原始链表中存储的有可能是很大的对象,而索引结点只需要存储关键值值和几个指针,并不需要存储对象,因此当节点本身比较大或者元素数量比较多的时候,其优势必然会被放大,而缺点则可以忽略。
redis只在两个地方用到了跳跃表,一个是实现有序集合键,另一个是在集群阶段中用作内部数据结构,跳跃表再redis中没有其他用途,
Redis的跳跃表由zskiplistNode和skiplist两个结构定义,其中 zskiplistNode结构用于表示跳跃表节点,而 zskiplist结构则用于保存跳跃表节点的相关信息,比如节点的数量,以及指向表头节点和表尾节点的指针等等。
总结:
- 跳跃表基于单链表加索引的方式实现
- 跳跃表以空间换时间的方式提升了查找速度
- Redis有序集合在节点元素较大或者元素数量较多时使用跳跃表实现
- Redis的跳跃表实现由 zskiplist和 zskiplistnode两个结构组成,其中 zskiplist用于保存跳跃表信息(比如表头节点、表尾节点、长度),而zskiplistnode则用于表示跳跃表节点
- Redis每个跳跃表节点的层高都是1至32之间的随机数
- 在同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的成员对象必须是唯一的跳跃表中的节点按照分值大小进行排序,当分值相同时,节点按照成员对象的大小进行排序。
5、整数集合
整数集合是集合键的底层实现之一,当一个集合只包含整数值,并且这个集合元素的数量并不多时,redis就会使用整数集合作为集合键的底层实现。
其实现结构如下如所示:
其中:encoding表示编码方式,可以为INTSET_ENC_INT16、INTSET_ENC_INT32、INTSET_ENC_INT64;length保存了元素数量,contents为实际的数组。
总结:
- 整数集合是集合键的底层实现之一
- 整数集合的底层实现是数组,这个数组以有序、无重复的方式保存集合元素,在有需要的时候,程序会根据新添加元素的类型,改变这个数组的类型。
- 升级操作为整数集合带来了操作上的灵活性,并且尽可能的节约了内存。
- 整数集合只支持升级操作,不支持降级操作
6、压缩列表(待补充)
压缩列表是列表建和哈希键的底层实现之一,当一个列表建只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么redis就会使用压缩列表来做列表建的底层实现。
压缩列表是redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据接口。一个压缩列表可以包含任意多个节点,每个节点可以保存一个字节数据或者一个整数值。结构及含义如下图所示。
总结
- 压缩列表是一种为节约内存而开发的顺序型数据结构。
- 压缩列表被用作列表建和哈希键的底层实现之一。
- 压缩列表可以包含多个节点,每个节点可以保存一个字节数据或者整数值
- 添加新节点到压缩列表,或者从压缩列表中删除节点,可能会引发连锁更新操作,但这种操作出现的几率并不高。