Redis基础数据结构

Redis使用接近计算机底层的C语言编写而成,并且在c语言的基础上设计实现了多种基础数据结构,在实现功能的同时达到了非常出色的性能表现。Redis对外提供的String、List、Hash、Set和Zset五种Redis对象皆是基于其基础数据结构实现的。

Redis底层数据结构

简单动态字符串SDS
Redis并未使用C语言内部的字符串表示,而是使用自己构建的简单动态字符串(SDS)作为默认字符串实现。在Redis里,C字符串只会在无需对字符串值进行修改的地方使用,如打印日志。

SDS结构sdshdr内部包含三个属性:

buf为保存字符串的字节数组;
len表示buf数组中已使用的字节数量;
free表示buf数组中未使用的字节数量。

SDS相较于C字符串,具有以下优点:

1、N(1)复杂度获得字符串长度。
2、杜绝缓冲区溢出。C字符串不记录自身长度,所以在进行拼接造成时,可能造成缓冲区溢出。而当SDS相关API在对SDS进行修改前,会首先检查其空间是否满足修改要求,若不满足则会先进行空间扩展。
3、减少修改字符串带来的内存重分配次数。SDS通过空间预分配和惰性空间释放两种优化策略有效减少SDS因拼接或截断操作引起的内存重分配次数。

空间预分配:当SDS的API对SDS进行空间扩展时,不仅会分配修改所需的空间,还会额外分配未使用。具体策略是:当修改后SDS的len值小于1MB,则分配和len值相同的未使用空间;当修改后len值大于1MB时,则分配1MB的未使用空间。
惰性空间释放:当SDS的API需要缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是修改free将这些字节数量记录下来供后续使用。

4、二进制安全。C字符串以空字符串‘\0’来表示字符串的结束,因此其不能包含空字符串,进而不能保存像图片、音频、视频、压缩文件等得二进制数据。而SDS通过len来确定字符串的结束点,所以保存上述特殊数据格式没有任何问题。
5、兼容部分C字符串函数。

链表
Redis的链表数据结构为双向非循环链表,同时提供首尾指针。其被广泛用于实现Redis的各种功能,比如列表键、发布与订阅、慢查询、监视器等。

字典
字典又称符号表、关联数组或映射,是一种用于保存键值对的抽象数据结构。字典中的每个键都是独一无二的,程序可用在字典中根据键查找、更新或删除值。Redis数据库便是使用字典作为底层实现,同时它也是哈希键的底层实现之一,当哈希键包含的键值对较多或者键值对都是比较长的字符串时,Redis就会使用字典作为哈希键的底层实现。
Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,每个哈希表节点保存一个键值对。字典所用哈希表结构dictht包含四个属性:

table表示哈希表数组,数组中每个元素dictEntry表示一个哈希表节点,存储键值对,同时存储指向下一个节点的指针。Redis的哈希表使用拉链法解决冲突。
size表示哈希表大小;
sizemask=size-1位哈希表大小掩码,用于计算索引值;
used表示已有节点数量。

而字典结构dict也包含四个属性:

type,类型特定函数指针,针对不同类型的键值对,为创建多态字典而设置;
privdate,保存需要传给那些类型特定函数的可选参数;
ht[2],包含两个哈希表的数组,一般只使用ht[0]哈希表,ht[1]用于rehash时使用。
rehashidx,记录rehash目前进度,若没有进行rehash则为-1。

哈希表通过rehash操作对哈希表的大小进行扩展或者收缩,使负载因子在多次操作后仍维持在合理的范围内。若是扩展操作,则ht[1]大小为第一个大于等于ht[0].used*2的2次幂;若是收缩操作,则ht[1]的大小为第一个大于等于ht[0].used的2次幂。当ht[0]所有键值对都迁移到ht[1]后,则释放ht[0],将ht[1]置为ht[0],并在ht[1]新创建一个空白哈希表,为下一次rehash做准备。值得注意的是,整个rehash过程是渐进式的,rehashidx记录目前ht[0]已完成rehash的坐标位置,也就是rehash时ht[0]和ht[1]都会被使用到。

哈希表会在以下情况下进行扩展操作:
1、服务器目前没有在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于1;
2、服务器目前正在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于5。
哈希表会在负载因子小于0.1时进行收缩操作。

跳表skiplist
跳表是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。其支持平均O(logN),最坏O(N)复杂度的节点查找,还可通过顺序行操作来批量处理节点。跳表在Redis中的使用场景较少,仅用于实现Zset有序集合,以及在集群节点中用作内部数据结构。

跳表结构zskiplist拥有四个属性:

header,指向跳表的表头节点;
tail,指向跳表的表尾节点;
level,记录跳表中层数最大节点的层数;
length,记录跳表的长度,即跳表中包含节点的数量。

每一个跳表节点包含以下属性:

level,节点层数。每个节点在创建时会根据幂次定律(越大的数出现的概率越小)随机生成一个介于1-32之间的值作为节点层数。每一层都有一个指向表尾方向的前进指针,该指针指向表尾方向最近的一个层数等于当前层数的节点,另外每一层level还包含一个跨度span属性,表示前进指针所指节点与当前节点之间的距离,指向null的前进指针的span为0。
backward,后退指针。指向当前节点的前一个节点。后退指针在程序从表尾向表头便利时使用;
score,分值。决定节点在跳表中的顺序,跳表中节点按照各自分值从小到大排列,分值相同的节点按字典顺序从小到大排列;
object,指向一个字符串对象的指针,不同节点的分值可以一样,同分值的节点按照对象字典序排序,较小的排在前面。

整数集合intSet
整数集合是Redis底层用于保存整数值的集合抽象数据类型,可保存int_16t,int_32t和int_64t的整数值,并保证集合内不出现重复元素。

整数集合intSet结构包含以下三个属性:

encoding,表示集合内整数的编码方式;
length,集合包含的元素数量;
contents,int_8t数组,用于保存集合元素,数组中各元素按值大小从小到大有序排列。数组中实际存储的整数类型由encoding决定。

集合升级
当把一个新元素加入整数集合,且这个元素的类型比集合里所有元素的类型都要长时,将会对整个集合进行升级,将所有元素的类型都转换为较长的新类型。集合升级首先要做的是根据新的类型长度和数组长度对数组空间进行重分配,然后将转换后的现有元素依次放置到正确的位置上,再将新元素加入到数组头部或尾部(新增元素长度更大,一定大于或小于所有现有元素)最后将encoding变为转换后的类型。升级策略既可以提升集合灵活性,又可以尽可能地节省内存。
另外,整数集合不支持降级操作,一旦升级,编码会一直保持升级后的状态。

压缩列表zipList
压缩列表是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表项,且每个列表项都是小整数值或短字符串时,Redis会使用压缩列表作为列表键的底层实现。

压缩列表由一下各部分组成:

zlbytes,记录整个压缩列表所占内存字节数,对压缩列表进行内存重分配或计算zlend位置时使用;
zltail,记录压缩列表最后节点到起始地址间的字节数,由此无需遍历即可获得表尾节点地址;
zllen,记录压缩列表包含节点数,当其等于65535时,真实节点数量需要遍历列表计算获得;
entryX,列表节点,长度不定,具体长度由保存内容决定;
zlend,特殊值0xFF,标记压缩列表末端。

而每个压缩列表节点entryX包含三个组成部分:

previous_entry_length,记录前一个节点长度的字节数。若前一个节点长度小于254字节,则此字段长度为一字节,否则此字段长度5字节,第一字节设为0xFE,后4字节记录前一节点长度。依靠这一字段,程序可以通过指针运算根据当前节点起始地址计算上一节点起始地址,从而实现表尾向表头的遍历;
encoding,记录节点所保存content的数据类型和长度。长度为1、2、5字节,且前两位为10、01、00表示是字节数组编码,剩余位数记录字节数组的长度。长度为1字节,且前两位为11表示是整数编码,整数的编码类型和长度由剩余位数表示;
content,保存节点的值,其可以是字节数组或者整数值。

连锁更新
由于previous_entry_length记录了上一节点的长度,当上一节点长度由小于254字节向上跃升时,则previous_entry_length的长度也会由1字节变为5字节,若压缩列表中很多节点长度都接近254字节,则可能在加入一个新节点可能导致多个节点的previous_entry_length长度增加,引起连锁反应,程序需要不断对压缩列表执行内存重分配。同理,删除节点可能导致previous_entry_length长度的减少,一样引起连锁更新,连锁更新最坏的时间复杂度为O(n^2),单其实际发生概率很低。

总结
本篇介绍了Redis用到的所有主要数据结构,但Redis并未直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构构建了一个对象系统,其中包含字符串对象,列表对象,哈希对象,集合对象和有序集合对象这五种对象,每一种对象都用到了至少一种上述基础数据结构作为底层实现。五种对象的具体介绍将在下篇记录。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值