-
-
地方简单动态字符串
-
redis没有直接使用C语言传统的字符串表示(以空字符结尾的字符数组),而是自己构建了一种名为简单动态字符串(simple dynamic string sds)的抽象类型,并将sds作为redis的默认字符串表示。
在redis中,c字符串只会作为字符串字面量用在一些无须对字符串值进行修改的地方。比如打印日志:
redisLog(REDIS_WRNING,"redis is now ready to exit,bye bye");
如果redis需要的是一个可以被修改的字符串,那么就会使用sds来表示字符串的值,比如,如果在客户端执行命令,msg 和 hello word都将会用sds来表示。
redis> set msg "hello word"
OK
除了用来保存数据库中的字符串值之外,sds还被用作缓冲区:AOF模块中的AOF缓冲区。
1.sds定义
每个sds.h/sdshdr结构表示一个sds值:
sds遵循了c字符串以空字符结尾的惯例,保存空字符的1字节空间不计算到len属性中。为空字符串分配额外的1字节空间,以及将空字符串添加到字符数组的末尾都是由sds函数自动完成的,所以空字符串对于sds的使用者来说是完全透明的。遵循了c字符串以空字符结尾的惯例的好处是,可以复用一部分的C字符串函数库里的函数。
2.sds与c字符串的区别
3.sds主要操作的API
-
链表
链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并且可以通过增删节点来灵活的调整链表的长度。
链表内置在很多高级语言里,但是C语言并没有内置这种结构,所以redis构建了自己的链表。
1.链表和链表节点的实现
每一个链表节点使用adlist.h/listNode结构来表示:
虽然仅仅使用多个listNode结构就可以组成链表,但是使用adlist/list来持有链表的话,操作起来会非常的方便:
list结构为链表提供了表头指针head,表尾指针tail,以及链表长度计数器,而dup,free,和match成员则是用于实现多态链表所需的类型特定函数:
dup函数用于复制链表节点所保存的值;
free函数用于释放链表节点所保存的值;
match函数用于对比此链表节点所保存的值和另外一个输入值是否相等。
redis实现的链表的特性总结如下:
1》链表节点带有prev和next指针,获取某个节点的前置节点和后置节点的复杂度都是O(1)
2》表头节点的prev和表尾节点的next指针都指向null,对链表的访问以null为终点。
3》获取表头节点和表尾节点的时间复杂度都是O(1)
4》获取节点数量的时间复杂度为O(1)
5》链表节点使用void*指针来保存节点值,并且可以通过dup,free,match三个属性为节点值设置类型特定函数,所以节点可以保存不同类型的值。
2.链表和链表节点的API
-
字典(相当于java中的map)
字典,又称符号表,关联数组,映射。是一种保存键值对的抽象数据结构。
每个字典中的键都是独一无二的,可以根据键查找和更新值。又或者根据键来删除整个键值对。
字典在redis中的应用相当的广泛,比如redis的数据库就是使用字典作为底层实现的,对数据库的增删改查操作也是构建在对字典的操作之上。
1.字典的实现
redis的字典使用哈希表作为底层实现,一个哈希表中可以有多个哈希表节点。每个哈希表节点就保存了字典中的一个键值对。
1.1哈希表
redis字典所使用的哈希表由dict.h/dictht结构定义:
table属性是一个数组,数组中的每一个元素都是一个指向dict.h/dictEntry的指针,每个dictEntry结构保存着一个键值对。size属性记录了哈西表的大小,而used属性则记录了哈希表目前已有节点(键值对)的数量。sizemask属性的值总是等于size-1,这个属性和哈希值一起决定一个键应该被放在哪一个索引上。下图展示了一个不包含任何键值对的哈希表。
1.2哈希表节点
哈希表节点使用dictEntry结构表示,每个dictEntry结构都保存着一个键值对。
key属性中保存着键值对中的key,val属性保存着键值对中的值,值得类型可以是指针,uint64,int64三种类型。
next属性是指向下一个哈希表节点的指针,这个指针可以将多个哈希表节点连接起来,以此来解决hash冲突的问题。
1.3字典
redis中的字典由dict.h/dict结构表示:
type属性和privdata属性是针对不同类型的键值对,为创建多态字典而设置的。
type属性是一个指向dictType的指针,每个dictType结构保存了一簇用于操作特点类型的键值对的函数,redis会为用途不同的字典设置不同的类型特定函数。
privdata属性保存了需要传给这些特定函数的可选参数。
ht属性表示一个包含两个项的数组,每一个项都表示一个哈希表,一般情况下,字典只使用ht[0],ht[1]会在对ht[0]resize的时候使用.rehashinx记录了rehash的进度,如果目前没有rehash,rehashinx的值为-1。
refresh:随着键值对的不断增多或者减少,太多的键值对会导致查找索引的性能低下,太少的键值太大的数组空间是内存的浪费。所以为了维持键值对和数组大小这两者的比例(用负载因子表示)。我们需要对哈希表进行相应的扩展和收缩。通过refresh来完成。
执行扩展操作的条件:
1》服务器目前没有执行bgsave或者bgrewriteaof命令,并且负载因子大于等于1。
2》服务器正在执行bgsave或者bgrwriteaof命令,并且负载因子大于等于5。
那么思考一个问题:将ht[0]中存在的键值对rehash到ht[1]中能一次性完成吗。答案是不行的,原因是如果键值对的数目非常的多,那么庞大的计算量将会使服务器在一段时间内停止服务,所以字典的refresh操作是渐进式的。具体步骤如下
渐进式的好处在于每一次对字典的增删改查操作的时候会refresh指定索引的键值对。而不是在达到扩展条件的时候就一次性的执行所有的refresh操作。这样子refresh的时候,服务器还能处理用户请求。
在渐进式refresh的过程中,字典会同时使用ht[0]和ht[1]两张哈希表,所以增删改查的操作是在两张表上进行。在一张表上找不到指定的键就去另一张表上找,然后执行相应的操作。
字典操作的API:
-
跳跃表
跳跃表是一个有序的数据结构,它通过在一个节点中维持多个指向其他节点的指针,从而达到快速访问的目的
跳跃表支持平均O(logN),最坏O(N)的复杂度的节点查找,还可以通过批量操作来批量处理节点。
大部分情况下,跳跃表的效率可以和平衡树相媲美,并且因为实现简单,所以有不少程序都用跳跃表来代替平衡树。
Redis使用跳跃表作为有序集合键的底层实现之一,如果一个有序集合包含的元素数量比较多,或者有序集合中元素成员是比较长的字符串的时候将会使用跳跃表作为有序集合的底层实现。
跳跃表中定位表头和表尾的时间复杂度为O(1),查找跳跃表长度的时间复杂度为O(1)
表头不在length(跳跃表长度)和level(跳跃表中最大层数)的计算范围内。
1.跳跃表的实现
2.层
跳跃表中的level数组可以包含多个元素,每个元素都包含一个指向其他节点的指针,程序可以通过这些层来加快对其他节点的访问速度,一般来说,层的数量越多,访问其他节点的速度就越快。
每次创建一个新跳跃表节点的时候,程序都要根据幂次定律(越大的数出现的概率越小)随机生成一个介于1到32之间的数来作为level数组的大小,这个大小就是数的高度。
3.前进指针
前进指针用于从表头向表尾方向访问节点,下图用虚线表示了程序从表头向表尾遍历跳跃表所有节点的路径:
4.跨度
跨度用于记录两个节点之间的距离,指向null的前进指针的跨度都为零。
在查找某个节点的过程中,将沿途所访问过的所有层的跨度加起来就是所查找节点的跨度。
5.后退指针
后退指针用于从表尾向表头方向访问节点,跟可以一次跳过多个节点的前进指针不同,每个节点的后退指针只有一个,所以只能后退到前一个节点。
6.分值和成员
节点中的分值是一个double类型的浮点数,跳跃表中的所有数都按照分值的大小从小到大排列。
节点的成员对象是一个指针,它指向一个字符串对象。
在同一个跳跃表中,各个节点保存的对象必须是不同的,而分值却可以是相同的,分值相同的节点将按照成员对象在字典序中的大小来进行排序,成员对象小的将会排在前面。
跳跃表API:
-
整数集合
整数集合是集合键的底层实现之一,当一个集合只包含整数元素,并且元素的数量不多的时候,redis就会使用整数集合作为集合键的底层实现。
1.整数集合的实现
contents是一个数值,其中的每个元素按照元素的大小从小到大排列,且不允许有重复的元素。
encodeing代表的是contents数组中的元素类型
length表示contents数组的长度
2.升级
当我们向整数集合添加值时,如果添加的值的类型比整数集合现在的值的类型的长度要长时。整数集合需要先进行升级(将集合中所有的元素的类型提升为和新插入类型一样的类型),之后再 插入新的元素。
添加的步骤分为三步进行
1》扩展整数集合:将新元素的(类型长度*原整数集合中元素个数+1)为新的数组的类型长度总和。
2》将原整数集合中的每个元素放到新数组的合适索引处,首先放的是原数组中最大的数,(因为新插入的数的类型长度大于原有元素的类型长度,所以新插入的数要么大于原集合中的所有数(为正数的时候),要么小于元集合中的所有数(为负数)),所以新插入的数如果是正数,那么原数组中最大的数在新数组中的索引为倒数第二个。新插入的数如果是负数,那么索引的位置是最后一个,再按照原数组元素从大到小的顺序依次将原数组中的元素移到合适的位置。
3》最后将新元素放到数组的最前面或者最后面(正数在最后)。
注意:整数集合不支持降级。当整数集合中存在的高长度类型的数据全部被删除,整数集合的类型长度也不会因此降低。
3.升级的好处
1》因为我们通常不会见不同类型长度的数放入同一种数据结构,为了避免在存数之前对数据类型进行转换,我们可以很方便的使用整数集合来自动帮我们转换类型。
2》整数集合可以在必要的时候对数据类型进行转换,而不是在一开始就开辟大的内存空间去存储低长度数据类型。这样做节约了内存。
整数集合的API
-
压缩列表
压缩列表是redis用于节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序性数据结构。一个压缩列表可以包含任意多个节点(entry),每个节点都可以保存一个字节数组,或者一个整数值。
1.压缩列表节点的构成
每个压缩列表节点都由previous_entry_length,enconding,content三部分组成。
1》previous_entry_length
这个属性的单位字节,记录了前一个节点的字节长度。只可以是1字节或者5字节。
如果前一个节点的长度小于254字节,那么此属性的长度为1字节。若果前一个节点的长度大于等于254字节,那么此属性的值长度为5字节,其中第一个字节会被设置为0XFE(254),之后的四个字节用于保存前一个节点的长度。
因为此属性记录了前一个节点的长度,所以程序可以通过当前节点的起始地址推算出下一个节点的起始地址。
2》encoding
这个属性的值记录了content数据的类型以及长度
3》content
这个属性负责保存节点的值,节点的值可以是一个字节数组或者整数。
如上图所示,encoding为00001011表示content的长度为11,encoding为11000000表示content为16_int类型的整数。
2.连锁更新
连锁更新的发生情况:
e1的previous_entry_length的值在添加节点前是1字节。但是添加节点后,因为新添加的节点的长度大于254字节,所以e1的previous_entry_length属性的长度扩展为5个字节。然而e1节点的总长度就超过了254个字节,这就导致了e2节点的previous_entry_length属性的值也会变为5个字节,e3,e4,en同理。导致了连锁更新。
因为连锁更新在最坏情况下需要对压缩列表执行N此空间重分配操作,而每次空间重分配的最坏时间复杂度为O(N),所以连锁更新的最坏时间复杂度为O(N)
虽然连锁更新的时间复杂度较高,但是发生的概率很低。要满足压缩列表里恰好有连续多个,长度介于250到253字节的节点的条件,连锁更新才能发生。少量节点的连锁更新也不会对性能造成影响。
压缩列表的API