Redis的底层数据结构之SDS,链表,字典,跳跃表,整数集合,压缩列表

深入了解Redis底层数据结构
说明
说到Redis的数据结构,我们大概会很快想到Redis的5种常见数据结构:字符串(String)、列表(List)、散列(Hash)、集合(Set)、有序集合(Sorted Set),以及他们的特点和运用场景。不过它们是Redis对外暴露的数据结构,用于API的操作,而组成它们的底层基础数据结构又是什么呢?

简单动态字符串(SDS)
链表
字典
跳跃表
整数集合
压缩列表

Redis的GitHub地址github.com/antirez/red…

简单动态字符串(SDS)
Redis是用C语言写的,但是Redis并没有使用C的字符串表示(C是字符串是以\0空字符结尾的字符数组),而是自己构建了一种简单动态字符串(simple dynamic string,SDS)的抽象类型,并作为Redis的默认字符串表示

在Redis中,包含字符串值的键值对底层都是用SDS实现的

SDS的定义
SDS的结构定义在sds.h文件中,SDS的定义在Redis 3.2版本之后有一些改变,由一种数据结构变成了5种数据结构,会根据SDS存储的内容长度来选择不同的结构,以达到节省内存的效果。

SDS与C字符串的区别
C语言使用长度为N+1的字符数组来表示长度为N的字符串,字符数组的最后一个元素为空字符’\0’,但是这种简单的字符串表示方法并不能满足Redis对于字符串在安全性、效率以及功能方面的要求,那么使用SDS,会有哪些好处呢

常数复杂度获取字符串长度

C字符串不记录字符串长度,获取长度必须遍历整个字符串,复杂度为O(N);而SDS结构中本身就有记录字符串长度的len属性,所有复杂度为O(1)。Redis将获取字符串长度所需的复杂度从O(N)降到了O(1),确保获取字符串长度的工作不会成为Redis的性能瓶颈

杜绝缓冲区溢出,减少修改字符串时带来的内存重分配次数

C字符串不记录自身的长度,每次增长或缩短一个字符串,都要对底层的字符数组进行一次内存重分配操作。如果是拼接append操作之前没有通过内存重分配来扩展底层数据的空间大小,就会产生缓存区溢出;如果是截断trim操作之后没有通过内存重分配来释放不再使用的空间,就会产生内存泄漏

而SDS通过未使用空间解除了字符串长度和底层数据长度的关联,3.0版本是用free属性记录未使用空间,3.2版本则是alloc属性记录总的分配字节数量。通过未使用空间,SDS实现了空间预分配和惰性空间释放两种优化的空间分配策略,解决了字符串拼接和截取的空间问题

二进制安全

C字符串中的字符必须符合某种编码,除了字符串的末尾,字符串里面是不能包含空字符的,否则会被认为是字符串结尾,这些限制了C字符串只能保存文本数据,而不能保存像图片这样的二进制数据

而SDS的API都会以处理二进制的方式来处理存放在buf数组里的数据,不会对里面的数据做任何的限制。SDS使用len属性的值来判断字符串是否结束,而不是空字符

兼容部分C字符串函数

虽然SDS的API是二进制安全的,但还是像C字符串一样以空字符结尾,目的是为了让保存文本数据的SDS可以重用一部分C字符串的函数

链表
链表是一种比较常见的数据结构了,特点是易于插入和删除、内存利用率高、且可以灵活调整链表长度,但随机访问困难。许多高级编程语言都内置了链表的实现,但是C语言并没有实现链表,所以Redis实现了自己的链表数据结构

链表在Redis中应用的非常广,列表(List)的底层实现就是链表。此外,Redis的发布与订阅、慢查询、监视器等功能也用到了链表

字典
字典,又称为符号表(symbol table)、关联数组(associative array)或映射(map),是一种用于保存键值对(key-value pair)的抽象数据结构。字典中的每一个键都是唯一的,可以通过键查找与之关联的值,并对其修改或删除

Redis的键值对存储就是用字典实现的,散列(Hash)的底层实现之一也是字典

跳跃表
一个普通的单链表查询一个元素的时间复杂度为O(N),即便该单链表是有序的。使用跳跃表(SkipList)是来解决查找问题的,它是一种有序的数据结构,不属于平衡树结构,也不属于Hash结构,它通过在每个节点维持多个指向其他节点的指针,而达到快速访问节点的目的

跳跃表是有序集合(Sorted Set)的底层实现之一,如果有序集合包含的元素比较多,或者元素的成员是比较长的字符串时,Redis会使用跳跃表做有序集合的底层实现

跳跃表的定义
跳跃表其实可以把它理解为多层的链表,它有如下的性质

多层的结构组成,每层是一个有序的链表
最底层(level 1)的链表包含所有的元素
跳跃表的查找次数近似于层数,时间复杂度为O(logn),插入、删除也为 O(logn)
跳跃表是一种随机化的数据结构(通过抛硬币来决定层数)
那么如何来理解跳跃表呢,我们从最底层的包含所有元素的链表开始,给定如下的链表

然后我们每隔一个元素,把它放到上一层的链表当中,这里我把它叫做上浮(注意,科学的办法是抛硬币的方式,来决定元素是否上浮到上一层链表,我这里先简单每隔一个元素上浮到上一层链表,便于理解),操作完成之后的结构如下

查找元素的方法是这样,从上层开始查找,大数向右找到头,小数向左找到头

上面是查找元素,如果是添加元素,是通过抛硬币的方式来决定该元素会出现到多少层,也就是说它会有 1/2的概率出现第二层、1/4 的概率出现在第三层…

跳跃表节点的删除和添加都是不可预测的,很难保证跳表的索引是始终均匀的,抛硬币的方式可以让大体上是趋于均匀的

跳跃表的删除很简单,只要先找到要删除的节点,然后顺藤摸瓜删除每一层相同的节点就好了

跳跃表维持结构平衡的成本是比较低的,完全是依靠随机,相比二叉查找树,在多次插入删除后,需要Rebalance来重新调整结构平衡

跳跃表的实现
Redis的跳跃表实现是由redis.h/zskiplistNode和redis.h/zskiplist(3.2版本之后redis.h改为了server.h)两个结构定义,zskiplistNode定义跳跃表的节点,zskiplist保存跳跃表节点的相关信息

level数组(层):每次创建一个新的跳表节点都会根据幂次定律计算出level数组的大小,也就是次层的高度,每一层带有两个属性-前进指针和跨度,前进指针用于访问表尾方向的其他指针;跨度用于记录当前节点与前进指针所指节点的距离(指向的为NULL,阔度为0)
backward(后退指针):指向当前节点的前一个节点
score(分值):用来排序,如果分值相同看成员变量在字典序大小排序
obj或ele:成员对象是一个指针,指向一个字符串对象,里面保存着一个sds;在跳表中各个节点的成员对象必须唯一,分值可以相同
zskiplist结构

header、tail表头节点和表尾节点
length表中节点的数量
level表中层数最大的节点的层数
假设我们现在展示一个跳跃表,有四个节点,节点的高度分别是2、1、4、3

整数集合
整数集合(intset)是Redis用于保存整数值的集合抽象数据结构,可以保存类型为int16_t、int32_t、int64_t的整数值,并且保证集合中不会出现重复元素

整数集合是**集合(Set)**的底层实现之一,如果一个集合只包含整数值元素,且元素数量不多时,会使用整数集合作为底层实现

压缩列表
压缩列表(ziplist)是为了节约内存而设计的,是由一系列特殊编码的连续内存块组成的顺序性(sequential)数据结构,一个压缩列表可以包含多个节点,每个节点可以保存一个字节数组或者一个整数值

压缩列表是列表(List)和散列(Hash)的底层实现之一,一个列表只包含少量列表项,并且每个列表项是小整数值或比较短的字符串,会使用压缩列表作为底层实现(在3.2版本之后是使用quicklist实现)

上面介绍了Redis的主要底层数据结构,包括简单动态字符串(SDS)、链表、字典、跳跃表、整数集合、压缩列表。但是Redis并没有直接使用这些数据结构来构建键值对数据库,而是基于这些数据结构创建了一个对象系统,也就是我们所熟知的可API操作的Redis那些数据类型,如字符串(String)、列表(List)、散列(Hash)、集合(Set)、有序集合(Sorted Set)

根据对象的类型可以判断一个对象是否可以执行给定的命令,也可针对不同的使用场景,对象设置有多种不同的数据结构实现,从而优化对象在不同场景下的使用效率

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值