我们知道redis里面的每个键值对都是有对象组成的。其中,键总是字符串对象,而值可以是字符串对象、列表对象、哈希对象、集合对象和有序对象 这五个对象的任意一种。下面将会分别介绍以上涉及到的五种对象的底层数据结构。
1)简单动态字符串(SDS)
区别于C语言的字符串,redis构建了简单动态字符串SDS作为默认的字符串表示,来看一下SDS的定义:
上左图就是SDS的结构定义,右图中举例的
free为0 ,代表这个SDS没有分配任何未使用的空间;
len为5,表示这个SDS保存了一个五字节字长的字符串;
buf是一个char类型的数组,最后保存空字符 '\0' 字节;
SDS与C字符串的区别:
①获取字符串长度的时间复杂度
C字符串不记录自身长度信息,获取字符串长度的时间复杂度是O(N),SDS直接访问len属性即可获取长度信息,复杂度为0(1)。
②杜绝缓冲区溢出
C字符串容易造成缓冲区溢出,具体可以看下面这个例子(例子来自《redis设计与实现》 ):
SDS完全杜绝了出现缓冲区溢出的可能性:当API需要最SDS做修改时,API会先对SDS的空间做检查,如果不满足的话,SDS会自动对其做扩展,然后才会执行相应的修改,举个例子:
③减少修改字符串是带来的内存重分配次数
对于C字符串来说,每次修改都意味着一次内存重分配操作;SDS通过未使用空间实现了空间预分配和惰性空间释放两种优化策略。
空间预分配:当SDS的API对其做修改时,并且需要扩展空间时,程序不仅会分配所需要的空间,还会分配额外的未使用空间。
惰性空间释放:优化了SDS的字符串缩短操作,当SDS的API对其做缩短操作时,程序不会立即释放掉多余的空间,而是使用free属性,将这些缩短的空间大小记录下来,并等待进来使用,举个例子:
④二进制安全
C字符串除了结尾不能出现空字符,所以导致C字符串不能保存图片、视频、音乐等二进制数据,SDS程序不会对任何数据做过滤,可以用来保存图片、视频等数据信息。
⑤兼容部分C字符串函数
因为SDS保留了C的风格,末尾一空字符结尾,就是为了兼容部分的C函数。
总结,如下图:
2)链表
每一个链表节点使用listNode节点来表示,如下图
链表的结构如下图
每一个链表节点由一个listNode来表示,每一个节点都有一个指向前置和后置的指针,所以redis链表实现的是双端链表;
每一个链表都使用一个list结构来表示,这个结构带有链表头、表尾和长度等信息;
链表表头节点前置和表尾节点的后置节点都指向NULL,所以是无环的;
通过链表设置不同类型的特定函数,所以redis链表可以用来保存不同类型的信息;
3)字典
redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,每一个哈希表节点就保存一个键值对,接下来分别看一下哈希表、哈希表节点以及字典的实现。
哈希表的结构如下:
table是一个数组,每个元素指向一个哈希表节点,下图表示一个大小为4的空哈希表(没有任何键值对)
哈希表节点:每一个哈希表节点保存着一个键值对,如下图:
字典的数据结构如下:
ht保护两个项哈希表的数组,一般情况下字典只适用h[0],h[1]哈希表只会在对h[0]进行rehash时使用;
另外要提一下哈希算法,当要有一个新的键值对添加到字典的时候,先会计算出哈希值和索引值,然后根据索引值将键值对放到哈希表数组指定索引上面。举个例子,如果要将一个新的键值对KO和VO,插到字典里面,如下图所示:
那么这里就会出现一个问题,键冲突,redis是如何解决的呢?redis的哈希表是通过链地址法来解决冲突的。如下图,被分配到同一个索引上的多个节点用单向链表链接起来:
对与哈希表的扩展或者收缩,redis是采取rehash(重新散列)来完成的,具体的步骤如下:
为了 避免rehash对性能造成影响,redis是采用渐进式rehash进行操作的,具体步骤如下解释:
4)跳跃表
跳跃表是由zskiplistNode和zskiplist定义的,zskiplistNode结构用来表示跳跃表节点,zskiplist用来表示保存跳跃表节点的相关信息,zskiplistNode的定义如下:
zskiplist的结构如下:
下图表示带有zskiplist结构的跳跃表
每一个跳跃表节点的层高都是1-32之间的随机数。
同一个跳跃表中,多个节点可以包含相同的分值,但每个的节点成员对象必须是唯一的。
跳跃表的节点按照分值大小来进行排序,当分值相同时,节点按照成员对象的大小进行排序。
5)整数集合
整数集合是redis用于保存整数值的集合抽象数据结构,并保证集合是有序的并且不会出现重复元素。结构定义如下:
其中编码方式有3种,int16_t、int32_t、int64_t。同样的,举个例子:
下面介绍一下整数集合的升级操作,如果新加入的新元素的类型比现有集合的元素类型都要长时,整数集合需要升级操作,才能将新元素添加到整数集合里面。具体升级步骤如下:
另外需要说明的一点是,整数集合不支持降级。一旦对数组进行升级,编码就会一直保持升级后的状态。
6)压缩列表
压缩列表是由一系列特殊编码的连续内存块组成的顺序型数据结构,一个压缩列表可以包括多个节点,每个节点可以保持一个字节数组或者一个整数值,具体组成和说明如下:
压缩列表节点的构成如下:
- prev_entry_len:记录前驱节点的长度。
- encoding:记录当前节点的value成员的数据类型以及长度。
- value:根据encoding来保存字节数组或整数。
另外介绍要提高一种情况就是连锁更新:
连锁更新的两种情况:
- 如果前驱节点的长度小于254,那么prev_entry_len成员需要用1字节长度来保存这个长度值。
- 如果前驱节点的长度大于等于254,那么prev_entry_len成员需要用5字节长度来保存这个长度值。
如果一个压缩列表中,有多个连续、长度介于250字节到253字节之间的节点,因此记录这些节点只需要1个字节的prev_entry_len,如果要插入一个长度大于等于254的新节点到压缩列表的头部,然而原来的节点的prev_entry_len成员长度仅仅为1个字节,无法保存新节点的长度,因此会对新节点之后的所有prev_entry_len成员大小为1字节的节点产生连锁更新。同样的,如果一个压缩列表中,是多个连续的长度大于等于254的节点,当往压缩列表的头部插入一个长度小于254的节点,也会产生连锁更新。另外删除节点也会产生连锁更新。
在redis中,只处理第一种情况,不处理因为节点的变小而引发的连锁更新,防止出现反复的缩小-扩展。
以上主要介绍了后面会用到的几种数据结构,了解了这个,对下篇谈到的redis的五种对象的理解就会更容易一点。