第一部分 数据结构与对象
一、简单动态字符串
redis自己构建一种SDS的抽象类型,用作redis的默认字符串,可以用SDS的API操作
1.1 作用:
-
redis> SET msg “hello”
- 键是一个字符串对象,对象底层实现一个保存字符串“msg”的SDS
- 值是一个字符串对象,对象底层实现一个保存字符串“hello”的SDS
-
作用于缓冲区
- AOF模式中的缓冲区
- 客服端的输入缓冲区
1.2. 定义:
1.3. 与c语言获取str长度的区别:
- SDS有len属性,获取的复杂度为O(1) 。 c语言为O(N)
- c不知道str长度修改时容易溢出。 SDS为先检查长度是否够,不够就扩展
1.4. 减少修改字符串时带来的内存重新分配次数:
c语言每次增长或者缩短一个字符串,都要内存重分配,不做就会内泄漏。SDS通过未使用空间free解除了字符串长度和底层数组长度的关联
-
空间预分配
- 如果对SDS进行修改后,SDS的长度len<1M 那么也分配给free==len
- 如果对SDS进行修改后,SDS的长度len>=1M 那么也分配给free=1M
-
惰性空间释放
- 如果缩短一个字符串,那么会把删除的位置放到free里面
1.5. 二进制安全:
-
c语言字符串必须符合某种编码(比如ASCII)中间不能有空字符,会被认为是结尾所以不能保存图片、音频、视频等文件
-
SDS是二进制安全的,以二进制的方式处理buf数组里的数据,写入什么读取就是什么
1.6. 兼容部分c字符串函数:
遵循C字符串以控制符结尾的惯例 这样是为了可以重用c语言的一部分库函数
1.7.小结:
二、链表
2.1. 链表与链表节点的实现
链表提供了高效的节点重排能力,链表在Redis中的应用非常广泛,比如列表键的底层实现之一就是链表。当一个列表键包含了数量比较多的元素,又或者列表中包含的元素都是比较长的字符串时,Redis就会使用链表作为列表键的底层实现。
每个链表节点使用一个adlist.h/listNode结构来表示
多个listNode通过prev和next指针组成双端链表
用list来持有链表操作会更方便
2.2.小结
三、字典
字典,乂称为符号表( symbol table )、关联数组( associative array )或映射( map ),是 一种用于保存键值对(key-value pair)的抽象数据结构
3.1.哈希表
-
tabel属性是一个数组,每个元素指向dict.h/dictEntry结构的指针,dictEntry存放键值对。
-
size记录哈希表的大小,即table数组的大小
-
used记录目前已有(键值对)的数量
-
sizemask属性和哈希值一起决定一个键应该被放到table数组上的哪个索引上面
3.2.哈希表节点
存放键值对,next是指向下一个哈希表节点的指针(当他们的键的哈希值相等时),解决键冲突问题。
3.3.字典
type属性和privdata示性式针对不同类型的键值对,为创建你多态字典设置
-
type属性是一个指向dictType结构的指针,每个dictType结构保存了一簇用于操作特定类型键值对的函数,Redis会为用途不用的字典设置不同的类型特定函数。
-
privddata属性则保存了需要传给那些类型特定函数的可选参数
-
ht属性是一个包含两个项的数组,数组的每一个项都是一个dictht哈希表,一般情况下,字典只使用ht[0]哈希表,ht[1]哈希表只会在对ht[0]哈希表进行rehash时使用
-
rehashidx,记录目前的进度,如果没有在进行rehash,那么他的值为-1
3.4.哈希算法
当一个新的键值对添加到字典时,要根据键算出哈希值和索引值,然后将新键值对的哈希表节点放到哈希表数组的指定索引上面。
3.5.解决键冲突
- 相同的哈希值的键可以用next链接起来形成单向链表
- 因为dictEntry节点组成的链表没有指向链表表尾的指针,所以考虑速度,总是将新节点添加到链表的表头位置(复杂度O(1)),排在其他已有节点的前面。k2 v2就是新插入的
3.6.rehash
哈希表保存的键值对会增多或者减少,为了让负载因子维持在一个合理的范围之内,当键值对的适量太多或者太少,就需要对哈希表的大小进行扩展或收缩,可以通过rehash(重新散列)操作来完成。
步骤:
- 为字典的ht[1]哈希表分配空间,这个哈希表的空间大小取决于要执行的操作,以及ht[0]当前包含的键值对数量(也即是ht [ 0 ] . used属性的值):
-
如果执行的是扩展操作,那么ht [ 1 ]的大小为第一个大于等于ht [ 0 ] . used*2 的2”( 2的n次方幂);
-
如果执行的是收缩操作,那么ht [1]的大小为第一个大于等于ht [0] .used的2”
-
- 将保存在ht [0]中的所有键值对rehash到ht [1]上面:rehash指的是重新计算键的哈希值和索引值,然后将键值对放置到ht[l]哈希表的指定位置上。
- 当ht[0]包含的所有键值对都迁移到了 ht[l]之后(ht[0]变为空表),释放 ht[0],将ht[l]设置为ht[0],并在ht[l]新创建一个空白哈希表,为下一次rehash 做准备。
1)ht[0].used当前的值为4,4*2=8,所以ht[1]设置成8
2)将ht[0]包含的四个键值对都rehash到ht[1]
3)释放ht[0],并将ht[1]设置成ht[0]
3.7.渐进式rehash
为避免rehash对服务器性能造成影响,服务器不是一性将ht[0]里面的所有键值对全部rehash到ht[1],而是分多次,渐进式的将ht[0]里面的键值对慢慢地rehash到ht[1]
一次完整的rehash过程:
3.8.小结
四、跳跃表
跳跃表是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问的目的。
redis使用跳跃表作为有序集合键的底层实现之一,集合数量多或者元素的成员是比较长的字符串时使用。
-
Redis只在两个地方用到了跳跃表
-
实现有序集合键
-
在集群节点中用作内部数据结构
4.1.跳跃表的实现
-
Redis跳跃表由redis.h/zskiplistNode和redis.h/zskiplist两个结构定义
1. zskiplistNode表示跳跃表节点
2. zskiplist保存跳跃表节点的相关信息(节点数量,表头节点,表尾节点)
-
最左边为zskiplist结构
1. header: 指向跳跃表的表头节点
2. tail:指向跳跃表的表尾节点
3. level:记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不算)
4. length:记录跳跃表的长度,跳跃表目前包含节点的数量(表头节点不计) -
最右边的四个zskiplistNode
1. level:节点用L1,L2,L3等标记各个层(L1第一层) 每层两个属性:前进指针和跨度
2. 后退指针:BW标记为后退指针
3.分值(score):节点按各自保存的分值从小到大排列
4.成员对象:各个节点中的o1,o2,o3是节点所保存的成员对象
4.2.跳跃表节点
-
层
1. 一般来说,层数越多,访问其他节点速度越快
2.创建新节点,通过幂次定律随机生成一个1-32之前的值为level数组的大小,也就是高度 -
前进指针以及遍历过程
-
跨度:level[i].span属性 用于记录两个节点之间的距离(这是与普通跳跃表的区别之一)。因为在有序集合支持的命令中,有些跟元素在集合中的排名有关,比如获取元素的排名,根据排名获取、删除元素等。通过跳跃表结点的层跨度,可以快速得到该结点在跳跃表中的排名(排位rank)
-
后退指针(与普通跳跃表的第二个区别)用于从表尾向表头访问节点。首先通过tail到最尾节点,然后向前找
-
分值和成员:
- 分值(score)是一个double类型的浮点数,所有节点按照分值从小到大排序
- 成员对象(obj)是一个指针,指向一个字符串对象,字符串对象保存一个SDS值
- 对象必须唯一,分值可以相同(分值相同的节点按照成员对象在字典序中的大小来排序)
4.3.跳跃表
只靠多个跳跃表节点可以组成一个跳跃点,但是使用zskiplist结构来持有这些节点,可以更方便操作
- header和tail指针分别指向表头和表尾,定位到表头和表尾的复杂度为O(1)。
- length记录节点数量,可以O(1)复杂度内返回跳跃表的长度
- level属性用于在O(1)复杂度获取最高层的节点的成数量(表头不算)
4.4.小结
五、整数集合
整数集合是集合键的底层实现之一,当一个集合会包含整数值元素,并且这个集合的元素数量不多时redis使用整数集合作为集合键的底层实现。
5.1.整数集合的实现
保存的类型为int16_t、int32_t、int64_t的整数值,并且保证不会出现重复元素
- congtents数组是底层实现,每个元素都是contents数组的一个数组项,各个项在数组中按值的大小从小到大有序的排列,且没有重复项
- 虽然声明为int8_t类型的数组,但实际取决encoding属性(16,32,64)
encoding属性的值为INTSET_ENC_INT16 每个元素的类型为int6_t,所以contents大小为sizeof(int16_t)*5=16*5=80
5.2.升级
当我们向一个整数集合类型比较小的数组(int16_t)中加入一个比目前长的类型时(int32_t),整数集合需要先升级才能将新元素添加到整数集合。
步骤:
- 根据新元素类型扩展数组的大小,并未新元素分配空间
- 把原来数组类型换成现在的类型,且把元素放到正确的位置上,有序性不变
- 将新元素添加到底层数组里面
每次添加元素都可能升级,每次升级都需要对数组中已有元素进行类型转换,所以添加的时间复杂度为O(n)
5.3.升级的的好处
- 提升灵活性
c语言的限制我们不会将不同类型放在一起,但是通过这种方式就可以了 - 节约内存
- 不支持降级操作
六、压缩列表
压缩列表是列表和哈希键的底层实现之一。列表键只含有少量列表项,并且列表项要么小整数值,要么长度比较短的字符串。哈希键包含的所有键和值都是小整数值或者短字符串。
6.1.压缩列表的构成
由一系列特殊编码的连续内存块组成的顺序型数据结构。
一个压缩裂列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值
6.2.压缩列表节点的构成
字节数组长度选择:
<=63(2^6-1)
<=16383(z^14-1)
<=(2^32-1)
整数值长度选择:
4位长,0-12间的无符号整数
1字节或者3字节的有符号整数
int16_t 、int32_t、 int64_t
previous_entry_length:
以字节为单位记录前一个字节的长度
前一个字节长度<254字节,则previous_entry_length长度为1
前一个字节长度>=254字节,则previous_entry_length长度为5
第一个字节会设置为0xFE(后4字节保留前一字节的长度)
从表尾遍历到表头使用下面的原理实现,一直回溯到头
encoding
记录节点的content保存的类型和长度
1字节,2字节,5字节长,值的最高位为00,01,10是字节数组编码
1字节,值的最高位为11开头的是整数编码
上面两种数组的长度都由编码除去最高两位之后的其他记录
content
保存节点的值,根据上面的encoding表对照
6.3.连锁更新
当所有的节点长度都为250-253字节之间,所以他们保存的都是1字节长的previous_entry_length。如果讲一个长度>=254的节点设置为表头节点,后面的e1节点保存的previous_entry_length由1变成5,后面的e2,e3…都会更新。这个过程叫连锁更新。
删除节点也可能导致连锁更新—e1-en是250-253 ,big长度>=254,small<254的情况
6.4.小结
七、对象
redis并没有直接使用前面介绍的数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个对象系统包含字符串对象,列表对象,哈希对象,集合对象和有序集合对象
7.1.1.对象的类型与编码
键和值都是对象。每个对象都由redisObject结构表示
7.1.2.类型
键总是一个字符串对象,而值可以是字符串对象,列表对象,哈希对象,集合对象和有序集合对象的一种
可以用TYPE命令来获取键对应的值对象的类型
7.1.3.编码和底层实现
对象的ptr指针指向对象的底层实现数据结构,但是这个数据结构由对象的encoding属性决定。
每种类型的对象至少使用了两种不同的编码
用OBJECT ENCODING命令可以查看一个数据库键的值对象的编码
通过encoding属性设定对象所使用的编码,提高了灵活性和效率,redis可以根据不同的场景来为一个对象设置不同的编码。
7.2.字符串对象
字符串对象的编码可以使int、raw、embstr。
如果是整数值,会将ptr属性里面(将void*变为long)编码变为int
如果是字符串,并且长度大于32字节,将用SDS保存,编码为raw
如果是字符串,并且长度小于等于32字节,编码为embstr
embstr是专门保存段字符串的一种优化编码方式,raw会调用两次内存分配函数来创建redisObject结构和sdshdr结构,embstr只有一次
embstr的好处
再有需要的时候,程序会将保存的字符串对象转为浮点数,执行操作后再转为字符串。例如数字相加的时候。
编码的转换
int和embstr在某些条件下会变为raw编码
- int------>raw
如果这个字符串由于某些操作导致不再是整数值,比如append操作,就会变为raw编码。 - embstr(实际上是只读的)------>raw
当修改embstr的时候回先变为raw,然后再执行修改命令。所以修改命令后总会变为raw。
7.3.列表对象
列表对象的编码可以是ziplist或者linkedlist
- 每个压缩列表节点(entry)保存一个列表元素
- linkedlist每个节点保存一个字符串对象,每个字符串对象保存一个列表元素
注意:双端两边包含了多个字符串对象,字符串对象时Redis五种类型的对象中唯一一种会被其他四种类型对象嵌套的对象
编码转换
满足下面的条件-----ziplist 任意一个不满足转换linkedlist
- 列表对象保存的所有元素的长度都小于64字节
- 列表对象保存的元素数量小于512个
(上面的条件可以修改)
7.4.哈希对象
哈希对象的编码是ziplist或者hashtable
- ziplist的实现
- hashtable的实现(使用的是字典)
编码转换
满足下面的条件-----ziplist 任意一个不满足转换hashtable - 哈希对象保存的所有键值对的键和值的字符串的长度都小于64字节
- 哈希对象保存的键值对数量小于512个
(上面的条件可以修改)
7.5.集合对象
集合对象的编码是intset或者hashtable
- intset编码的集合对象中包含的所有元素保存在整数集合里
- hashtable底层是字典,每一个键是字符串对象,每个字符串对象包含了一个集合元素,值全部为NULL
编码转换
满足下面的条件-----intset 任意一个不满足转换hashtable - 集合对象保存的所有元素都是整数值
- 集合对象保存的元素数量不超过512个
(上面第二个条件可以修改)
7.6.有序集合对象
有序集合的编码是ziplist或者skiplist
- ziplist使用压缩列表作为底层实现,第一个保存元素的成员,第二个保存分值,从小到大排序
- skiplist使用zset结构作为底层实现(包含一个字典和一个跳跃表)
- 跳跃表可以实现对有序集合的范围操作ZRANK、ZRANGE等
- dict字典创建一个成员到分值的映射(键保存元素的成员,值保存元素的分值) 通过字典可以O(1)复杂度查找给定成员的分值ZSCORE
- 有序结合每个元素的成员都是一个字符串对象,每个元素的分值都是一个double类型的浮点数
- 虽然zset同是使用跳跃表和字典保存有序集合元素,但是他们都会通过指针来共享相同元素的成员和分值,所以不会产生任何重复成员或者分值,也不会因此浪费额外的外存。
注意:实际中上图的成员和分值是共享的,不会重复
编码转换
满足下面的条件-----ziplist 任意一个不满足转换skiplist
- 有序集合保存的元素数量小于128个
- 有序集合对象保存的所有元素成员的长度都小于64字节
(上面的条件可以修改)
8.11.小结
最后
本文是对学习redis数据结构的总结,参考书籍《redis设计与实现》