• 《Redis设计与实现》笔记1—数据结构与对象


    一、简单动态字符串SDS

    1、SDS的定义

    1、C字符串:在C语言中字符串实际上是以null字符串''来终止的一维字符数组;因此字符串以null结尾,并且包含了组成字符串的字符。而在Redis中,它有着自己的字符串结构,Redis只有在字符串不需要修改的时候使用C字符串,其余情况下都使用简单动态字符串(Simple dynamic string,SDS)

    2、如下结构表示一个SDS值:free为0表示没有分配任何未使用的空间,len为5表示保存了一个字节长度为5的字符串,buf表示一个char类型的数组,最后一个字节保存了空字符串''

    SDS遵循C字符串以空字符结尾的惯例,保存空字符串的1字节空间不计算在SDS的len属性中,并且分配空字符串到字符串末尾和分配额外的1字节空间,都是由SDS函数自动完成的。遵循这一惯例使得SDS可以直接重用一部分C字符串函数库中的函数

    2、SDS与C字符串的区别

    • 复杂度不同:C字符串并不记录自身的长度信息,所以获取字符串长度时需要遍历每个字符,复杂度为O(n);而SDS自身的len属性已经包含了长度信息,因而复杂度仅为O(1);设置和更新SDS长度的工作由SDS的API在执行时自动完成,无需手动进行修改;

    • 杜绝缓冲区溢出:C字符串在进行字符串拼接时,如未为目标字符串分配足够多的内存,会出现缓冲区溢出的情况;而SDS API会在操作之前判断空间大小,若空间较小,会自动将空间扩展至所需的大小;

    • 减少修改字符串带来的内存重新分配次数:每增加或减少一个C字符串,都需要重新分配内存,如果忽略这一步骤将会导致缓冲区溢出或内存泄漏问题;SDS则使用未使用空间解除字符串长度和底层数组长度之间的关联,即buff数组的长度并不一定就是字符数量加1,而是可以包含一些未使用的字节,字节的数量由free属性记录,通过未使用的空间,SDS实现了空间预分配惰性空间释放两种优化策略。

      空间预分配:①若SDS修改后的长度小于1MB,那么程序将分配和len属性同样大小的未使用空间,即len属性的值与free属性的值相同;②若SDS修改后的长度大于等于1MB,那么程序会分配1MB未使用的空间,举个例子来说:修改后的SDS长度未5MB,那么buf数组的实际长度将为5MB+1MB+1byte;这样在扩展SDS的字符串之前,SDS API都会先检查未使用空间是否足够,如果足够的话,API就会直接使用未使用空间,而无需执行内存重分配操作

      惰性空间释放:该策略用于优化SDS字符串缩短的操作,当SDS的API需要缩短SDS保存的字符串时,程序不立即使用内存重分配,而是使用free属性将这些字节的数量记录起来,并等待将来使用。通过惰性空间释放策略,SDS避免了缩短字符串时所需的内存重分配操作,为为将来可能由的增长操作提供了优化。与此同时,SDS也提供了响应的API,让我们在需要时真正的释放SDS未使用的空间

    • 二进制安全:C字符串中的字符必须符合某种编码(如ASCII编码),并且除了字符串末尾之外,字符串中不能包含空字符,否则最先被程序读入的空字符将被误认为是字符串结尾,这些限制使得C字符串只能保存文本数据,而不能保存像图片、音频、压缩文件之类的二进制数据。SDS的API都是二进制安全的,所有的API都会以处理二进制的方式处理SDS存放在buf数组中的一系列二进制数据,通过这类方使得Redis不仅可以保存文本数据,还可以保存任意格式的二进制数据

    • 兼容部分C字符串函数:SDS遵循C字符串以空字符结尾的惯例,它会将保存的末尾设置为空字符,并且总会为buf数组多分配一个字符来保存空字符,通过这一惯例,SDS可以在需要时重用<string.h>函数库,从而避免了不必要的重复代码

    综上所述,总结如下:

    C字符串 SDS
    获取字符串长度的复杂度为O(N) 获取字符串长度的复杂度为O(1)
    API是不安全的,可能会造成缓冲区溢出 API是安全的,不会造成缓冲区溢出
    修改字符串长度N次必然需要执行N次内存重分配 修改字符串长度N次最多需要执行N次内存重分配
    只能保存文本数据 可以保存文本或二进制数据
    可以使用所有<string.h>库中的函数 可以使用一部分<string.h>库中的函数

    3、SDS API

    主要的SDS API操作如下图所示:

    二、链表

    链表在Redis中的应用非常广泛,比如列表键的底层实现之一就是链表,当一个列表键包含了数量比较多的元素,或者列表中包含的元素都是比较长的字符串时,Redis就会使用链表作为列表键的底层实现。当然还包括发布与订阅,慢查询和监视器等

    1、链表和链表节点的实现

    1、每个listNode链表节点都由前置节点、后置节点、节点的值组成,多个链表可以通过前指针和后指针组成双向链表,使用多个链表节点可以组成链表,但操作起来并不是很方便。在Redis中通常使用list对象来持有链表,其结构如下图所示:

    • dup函数用于复制链表节点保存的值;
    • free函数用于释放链表节点所保存的值;
    • match函数用于对比链表节点所保存的值和另一个输入值是否相等;

    2、下图是一个list结构和三个listNode结构组成的链表:

    Redis的链表实现的特性总结如下:

    • 双端:链表节点带有prev和next指针,获取某节点的前置节点和后置节点的复杂度都是O(1);
    • 无环:表头节点的prev指针和表位节点的next指针都是指向Null,对链表的访问以NULL为终点;
    • 带表头指针和表位指针:通过list结构的head指针和tail指针,程序获取链表的表头节点和表位节点复杂度均为O(1);
    • 带链表长度计数器:程序使用list结构的len属性来对list持有的链表节点进行计数,因而获取节点数量的复杂度为O(1);
    • 多态:链表节点使用void*指针来保存节点的值,并且可以通过list节点的dup、free、match属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值

    2、链表和链表节点中的API

    如下图所示,为用于操作链表和链表节点的API

    三、字典

    字典又称符号表(Symbol table)、关联数组(associative array)或映射(map),是一种保存键值对的抽象数据结构。字典在Redis中的应用相当广泛,比如Redis数据库就是使用字典作为底层实现的,对数据库的增删改查操作也是构建在对字典的操作上的。字典还是哈希键的底层实现之一,当一个哈希键包含的键值对比较多,又或者键值对中的元素都是比较长的字符串时,Redis就会使用字典作为哈希键的底层实现

    1、字典的实现

    1、哈希表

    Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对。Redis字典所使用的哈希表由dictht结构定义,如下图:

    • table属性是一个数组,数组中的每个元素都是一个指向dicEntry结构的指针,而每个dicEntry结构保存着一个键值对;
    • size属性记录了哈希表大小,也就是table数组的大小;
    • userd属性记录的是哈希表目前已有节点(键值对)的数量;
    • sizemask数组的值总是等于size-1,这个属性和哈希值一起决定一个键应当被放到table数组的哪个索引上面

    2、哈希表节点

    哈希表节点使用dictEntry结构表示,每个dictEntry结构都保存着一个键值对,其结构如下图所示:

    • key属性保存着键值对中的键,它可以是一个指针,或者是一个unit64_t整数,又或者是一个int64_t整数;
    • V属性保存着键值对中的值;
    • next属性是指向另一个哈希表节点的指针,这个指针可以将多个哈希表相同的键值对连接在一起,来解决键冲突的问题;

    3、字典

    Redis中的字典由dict结构表示,如下图:

    • type属性是一个指向dictType结构的指针,每个dictType结构保存了一簇用于操作特定类型键值对的函数,Redis会为用途不同的字典设置不同的类型特定函数;

    • privdata属性保存了需要传递给那些类型特定函数的可选参数;

      type属性和privdata属性是针对不同类型的键值对,为创建多态字典而设置的;通常会配合使用,示例如下:

    • ht属性是一个包含两个项的数组,数组中的每个项都是一个dictht哈希表,一般情况下只会使用ht[0]哈希表,ht[1]哈希表只会在对ht[0]哈希表使用rehash时使用;
    • rehashidx属性也与rehash有关,它记录了rehash目前的进度,如果目前没有在进行rehash,它的值则为-1

    下图是一个普通状态下的字典:

    2、哈希算法

    当要将一个新的键值对添加到字典里面时,程序会先根据键计算出哈希值和索引值,再根据索引值将包含键值对的哈希节点放到哈希数组指定的索引上

    • 哈希值:hash=dict->type->hashFunction(key),使用字典设置的哈希函数计算key的哈希值
    • 索引值:index=hash&dict->ht[x].sizemask,使用哈希表的sizemask属性和哈希值计算出索引值,根据情况不同,ht[x]为ht[0]或ht[1];

    当字典被用作数据库的底层实现,或者哈希键的底层实现时,Redis使用MurmurHash2算法来计算键的哈希值,该算法的优点在于即使输入的键是有规律的,算法仍能给出一个很好的随机分布性,并且其计算速度也非常快

    3、解决键冲突

    当有两个或以上数量的键被分配到了哈希表数组的同一索引上面时,就会发生冲突。Redis的哈希表使用链地址法来解决冲突,每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,而被分配到同一索引上的多个节点可以用这个单向链表连接起来。

    因为dictEntry节点组成的链表没有指向链表表尾的指针,所以为了速度考虑,程序总是将新节点添加到链表的表头位置,复杂度为O(1)

    4、扩展收缩与rehash

    1、哈希表存在一个负载因子的概念,即load_factor=ht[0].userd/ht[0].size;

    2、当满足以下任意一个条件时,程序会自动开始对哈希表执行扩展操作:

    • 服务器没有在执行BGSave命令或BGReWriteAOF命令,并且哈希表的负载因子大于等于1;
    • 服务器目前正在执行的BGSave命令或者BGReWriteAOF命令,并且哈希表的负载因子大于等于5

    服务器是否在执行BGSave命令或者BGReWriteAOF命令会影响负载因子的大小,这是因为在执行上述命令时,Redis会创建当前服务进程的子进程,而大多数操作系统都采用写时复制技术来优化子进程的使用效率,通过提高所需的负载因子,从而尽可能地避免在子进程存在期间进行哈希表扩展操作,避免不必要的内存写入操作,最大限度地节约内存

    3、当哈希表的负载因子小于0.1时,程序自动开始对哈希表进行收缩操作;

    4、随着操作的不断进行,哈希表保存的键值对会逐渐地增多或减少,为了让哈希表的负载因子(load factor)维持在一个合理的范围内,当保存的键值对数量太多或太少时,程序需要对哈希表的大小进行相应的扩展或收缩,该工作通过执行rehash操作来完成,步骤如下:

    1. 为字典ht[1]哈希表分配空间,空间大小取决于要执行的操作类型和h[0]中当前包含的键值对数量(假设这个数字为m);如果是扩展操作,那么ht[1]的大小为第一个大于等于m*2的2的n次方幂;如果是收缩操作,那么ht[1]的大小为第一个大于等于m的2的n次方幂;
    2. 对保存在ht[0]中的数据重新计算键的哈希值和索引值,然后将键值对放置到ht[1]哈希表的指定位置上;
    3. 将ht[0]包含的所有键值对迁移到ht[1]后释放ht[0],将ht[1]设置为ht[0],并在ht[1]上创建一个新的空白哈希表,为下一次rehash做准备;

    5、渐进式rehash

    1、在进行rehash操作时,如果ht[0]内的键值对数量过于庞大,那么过大的计算量可能会导致服务器在一段时间内停止服务,所以rehash操作并不是一次性、集中式的完成,而是分多次、渐进式的完成的。详细步骤如下:

    1. 为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表;
    2. 在字典中维持一个索引计数器变量rehashindex,并将它的值设置为0,表示rehash工作正式开始;
    3. 在rehash进行期间,每次对字典进行添加、删除、查找或更新操作时,程序除了执行指定的操作以外,还会将ht[0]哈希表在rehashindex索引上的所有键值对rehash到ht[1],当rehash工作完成后,程序将rehashindex的属性值加一;
    4. 随着操作的不断进行,当ht[0]的所有键值对都被rehash到ht[1]时,程序的rehashindex属性值将设置为-1,表示操作已经全部完成;

    2、渐进式rehash操作的好处在于其采用分而治之的方式,将rehash所需的计算工作均摊到对每个字典的添加、删除、查找、更新操作上,避免了庞大的计算量;

    3、此外在渐进式rehash执行过程中,所有的删除、查找、更新操作都会在ht[0]和ht[1]两个哈希表上进行,而增加操作则一律添加至ht[1]中,以保证ht[0]中包含的键值对数量只简不增,直到rehash操作完成变为空表

    6、常用的字典API

    四、跳跃表

    跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点指针,从而达到快速访问节点的目的。跳跃表支持平均O(logN)、最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。大部分情况下,跳跃表的效率可以和平衡树相媲美,其实现更为简单。

    Redis使用跳跃表作为有序集合键的底层实现之一,如果一个有序集合包含的元素数量较多,又或者有序集合中元素的成员是比较长的字符串时,Redis就会使用跳跃表来作为有序集合键的底层实现。

    和链表、字典等数据结构被广泛应用在Redis内部不同,Redis只在两个地方用到了跳跃表,一个实现有序集合键,二是在集群节点中用作内部数据结构。

    1、跳跃表的实现

    跳跃表由zskiplistNode和zshiplist两个结构定义,zskiplistNode结构用于表示跳跃表的节点,而zskiplist结构则用于保存跳跃表节点的相关信息,如节点数量,指向表头节点和表表尾节点的指针等。如下图所示:

    左下角的是zskiplist结构,该结构包含以下属性:

    • header:指向跳跃表的表头节点;
    • tail:指向跳跃表的表尾节点;
    • level:记录跳跃表内,层数最大的节点的层数(表头节点的层数不计算在内)
    • length:记录跳跃表的长度,即当前的节点的数量(表头节点不包含在内)

    位于zskiplist右侧的是四个zskiplistNode结构,该结构包含以下属性:

    • 层(level):节点中使用L1、L2、L3等字样标记节点中的各个层。每层包括前进指针和跨度,前进指针用于访问位于表位方向的其它节点,而跨度则记录了前进指针所指向节点和当前节点的距离。上图中带数字的箭头就是前进指针,数字为跨度。程序从表头向表位进行遍历时,则会沿着层的前进指针进行;
    • 后退指针(backward):节点中用BW字样标注的就是后退指针,它指向当前节点的前一个节点,后退指针在程序从表尾向表头遍历时使用
    • 分值(score):各节点中的1.0,2.0,3.0就是节点所保存的分值,在跳跃表中,节点按各自所保存的分值从小到大排列;
    • 成员对象(obj):各个节点中的o1,o2,o3是节点保存的成员对象

    需要注意的是,表头节点和其它节点的构造是一样的,但是其后退指针,分值和成员对象属性不会被用到

    2、跳跃表节点

    跳跃表节点结构定义如下:

    1、层:跳跃表节点的level数组可以包含多个元素,每个元素都包含一个指向其它节点的指针,程序可以通过这些层来加快访问其它节点的速度,一般来说层数越多,访问其它节点的速度就越快。每次创建一个跳跃表节点,程序会根据幂次定律(越大的数出现的概率越小)随机生成一个介于1~32之间的值作为level数组的大小,大小也就是层的高度

    2、前进指针:每层都有一个指向表尾方向的前进指针,用于从表头向表尾方向访问节点。如下图,先访问第一个节点,从第四层的前进指针移动到第二个节点,再从第二层的前进指针移动到表中的第三个节点,再从第二层的前进指针移动到表中的第四个节点,第四个节点继续访问遇到null,即到达了表尾,结束本次遍历

    3、跨度:层的跨度用于记录两个节点之间的距离,指向null的所有前进指针其跨度都为0;跨度实际上是用来计算排位的(rank),如下图虚线,查找分值为2.0的节点时,经过了两个跨度为1 的节点,因此可以计算出,目标节点在跳跃表中的排位为2

    4、后退指针:每个节点都只有一个后退指针,所以每次只能后退至前一个节点

    5、分值:节点中的分值是一个double类型的浮点数,跳跃表中的所有节点都按分值从小到大来排序;

    6、成员对象:成员对象是一个指针,它指向一个字符串对象,而字符串对象则保存着一个SDS值

    在同一个跳跃表中,各个节点保存的成员对象必须是唯一的,但是多个节点保存的分值却可以是相同的,分值相同的节点将按照成员对象在字典序中的大小来排序,成员对象较小的节点会排在前面,成员对象较大的节点会排在后面

    3、跳跃表

    多个跳跃表节点可以组成一个跳跃表,跳跃表节点由zskiplist结构来持有,其结构定义如下:

    header指针和tail指针分别指向跳跃表的表头和表尾节点,length属性用来记录节点的数量,level属性可以用于在O(1)复杂度内获取跳跃表中层高最大的节点的层数量

    4、跳跃表API

    五、整数集合

    整数集合(intset)是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现

    1、整数集合的实现

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

    • intset结构将contents属性声明为int8_t类型的数组,但实际上contents数组并不包含任何int8_t类型的值,它取决于encoding属性的值,如果encoding属性的值为intset_enc_int16,那么contents就是一个int16_t类型的数组,同理还有可能为32位或64位
    • contents数组是整数集合的底层实现,整数集合每个元素都是该数组的一个item,各个项在数组中按值的大小从小到大有序地排列;
    • length属性记录了整数集合包含的元素数量,即contents数组的长度;

    2、升级与优势及降级

    1、当将一个新元素添加到整数集合中,并且新元素的类型比整数集合现有所有元素的类型都要长时,整数集合需要先进行升级,再进行添加操作;步骤如下:

    1. 根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间;
    2. 将底层数组现有的元素转换为新元素的类型,并将转换后的元素放置到正确的位置上,再放置的过程中,需要维持其有序性;
    3. 将新元素添加到底层数组中;

    2、整数集合升级策略有两个好处,一个提升整数集合的灵活性,另一个是尽可能地节约内存;

    因为C语言是静态类型语言,所以为了避免类型错误,通常不会将两种类型的值放到同一个数据结构中,而Redis中的整数集合可以通过升级的方式自适应整数类型来避免类型错误,保持灵活性;通过支持3种整数类型,可以使用最小支持类型节约内存,并确保必要时升级

    3、整数集合是不支持降级的,也就是说一旦数组进行了升级,编码就会一直保持升级后的状态;

    3、整数集合API

    六、压缩列表

    压缩列表(ziplist)是列表键和哈希键的底层实现之一,当一个列表键只包含少量的列表项,并且列表项要么是小整数值,要么是长度较短的字符串时,Redis就会使用压缩列表来做列表键的底层实现

    1、压缩列表的构成

    压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序列表(sequential)数据结构,一个压缩列表可以包含任意多个节点,每个节点可以保存一个字节数组或一个整数值,其结构及用途如下图所示:

    2、压缩列表节点的构成

    每个压缩列表可以包含一个字节数组或者一个整数值,字节数组可以是以下三种长度之一:①小于等于2^6-1字节②长度小于等于2^14-1字节;③长度小于等于2^32-1字节;整数值可以是以下六种长度之一:①4位长,0~12之间的无符号整数;②1字节长的有符号整数;③3字节长的有符号整数;④int16_t类型整数;⑤int32_t类型整数;⑥int64_t类型整数

    压缩列表节点由previous_entry_length、encoding、content三个部分组成

    1、previous_entry_length

    该属性以字节为单位,记录了压缩列表中前一个节点的长度,属性长度可以是1字节或5字节;如果前一节点的长度小于254字节,那么previous_entry_length属性的长度为1字节;如果前一节点的长度大于等于254字节,那么previous_entry_length属性的长度为5字节,并且属性中的第一字节会被设置为0xFE(254),之后的4个字节则用于保存前一节点的长度,如下图:

    因为节点的previous_entry_length属性记录了前一个节点的长度,所以程序可以通过指针运算,根据当前节点的起始地址来计算出前一个节点的起始地址

    2、encoding

    该属性记录了节点的content属性所保存数据的类型以及长度,根据字节数组编码和整数编码,分类如下:

    • 一字节、两字节、五字节,值得最高位为00,01或10的是字节数组编码,这种编码表示节点的content属性保存着字节数组,数组的长度由编码除去最高两位之后的其它位记录;
    • 一字节长,值的最高位以11开头的是整数编码,这种编码表示节点的content属性保存着整数值,整数值的长类型和长度由编码取出最高两位之后的其它位记录

    3、content

    该属性负载保存节点的值,节点值可以是一个字节数组或者整数,类型和长度由encoding属性决定

    3、连锁更新

    1、在一个压缩列表中,其节点长度都介于250~253之间,这时候新增一个长度大于等于254的节点并设置为表头,那压缩列表中的所有项都将依次后移,进行空间重分配,直至最后一个元素,这种连续多次空间扩展的操作即称为“连锁更新”;除了添加节点可能引发连锁更新外,删除节点也有可能引发连锁更新

    2、因为连锁更新在最坏情况下需要对压缩列表进行N次空间分配操作,而且空间重分配的最坏复杂度为O(N),所以连锁更新最坏复杂度为O(N^2);但这并不意味着它会造成性能问题,一是因为发生此类情况的概率很低,二是因为较少节点的情况下不会对性能造成影响。基于上述情况,ziplistPush等命令的平均复杂度为O(n)

    4、压缩列表API

    七、对象

    前面的章节依次介绍了Redis所用到的所有数据结构,但Redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,每个对象都用到了至少一种数据结构,在Redis执行命令前,我们会根据对象的类型来判断是否可以执行给定的命令。此外Redis基于引用计数机制实现了内存回收机制和对象共享机制,并且对象带有访问时间信息,在服务器启用了maxmemory功能的情况下,空转时长较大的键会被优先释放

    1、对象的类型和编码

    Redis使用对象来表示数据库中的键和值,每次创建一个键值对都会至少创建两个对象,一个对象用作键值对的键,一个对象用作键值对的值。每个对象都由redisObject结构表示,该结构中保存着如下属性:

    • type属性:记录的是对象的类型,可以是①字符串对象②列表对象③哈希对象④集合对象⑤有序集合对象中的一个;对于键值对来说,键总是一个字符串对象,值可以是其中的一种;我们可以使用type命令确认值对象的类型

    • encoding属性:对象的ptr指针指向对象的底层实现数据结构,而数据结构由对象的encoding属性决定;它记录了对象所使用的编码,而每种类型的对象都使用了至少两种不同的编码;我们可以使用object encoding命令查看值对象的编码

    通过设定encoding属性来设定对象使用的编码,极大的提高了Redis的灵活性和效率,因为Redis可以根据不同的使用场景来为一个对象设置不同的编码,从而优化对象的效率

    2、字符串对象

    1、字符串对象的编码可以实int、raw或embstr

    • 如果字符串对象保存的是整数值,并且整数值可以用long类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性中(将void*转换成long),并将字符串对象的编码设置为int;

    • 如果字符串对象保存的是一个字符串值,并且其长度大于39字节,那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串值,并将字符串对象的编码设置为raw

    • 如果字符串对象保存的是一个字符串值,并且其长度小于等于39字节,那么字符串对象将使用embstr编码来保存这个字符串值;embstr编码是专门用于保存短字符串的一种优化编码方式,它和raw一样都使用redisObject和sdshdr结构来表示字符串对象;

      它有如下优势:①raw编码会调用两次内存分配函数来创建上述两个结构,而embstr编码只需要调用一次;②释放embstr编码需要调用一次内存释放函数,而raw编码需要两次;③embstr编码的字符串对象所有的数据都保存在一块连续的内存中,所以能更好的利用缓存带来的优势

    • 用long double类型表示的浮点数也是作为字符串值来保存的,保存时会转换为字符串值,而在一些场景下使用时会转换为浮点值,使用完成有继续保存在字符串对象中

    总结如下:

    2、int编码和embstr编码的字符串对象会有一定条件下,转换为raw编码的字符串对象

    • 对于int编码的字符,如果执行的命令使这个对象保存的不再是整数值,而是一个字符串值,那么字符串对象的编码将从int变为raw,如执行Append操作
    • embstr编码的字符串对象没有对应的修改程序,所以embstr编码的字符串对象实际上时只读的,当执行修改命令时,程序会将其转换为raw对象,再执行转换操作

    3、字符串命令实现方法汇总如下:

    3、列表对象

    1、列表对象的编码可以是ziplist和linkedlist

    • ziplist编码的列表对象使用压缩列表作为底层的实现,每个压缩列表节点(entry)保存了一个列表元素;

    • linkedlist编码的列表对象使用双端链表作为底层实现,每个双端链表节点(node)都保存了一个字符串对象,每个字符串对象都保存了一个列表元素,这里注意字符串对象是Redis五种类型的对象中唯一一种会被其它四种对象嵌套的对象

    2、当列表对象同时满足以下两个条件时,列表对象使用ziplist编码,否则将使用linkedlist编码:①列表对象保存的所有字符串元素的长度都小于64字节;②列表对象保存的元素数量小于512个(需要注意的是这两个条件是可以通过配置文件中的list-max-ziplist-value和list-max-ziplist-entries进行修改的)

    3、列表键的值为列表对象,所以用于列表键的所有命令都是针对列表对象来构建的,以下为一部分列表键命令:

    4、哈希对象

    1、哈希对象的编码可以是ziplist或者hashtable

    • ziplist编码的哈希对象使用压缩列表作为底层实现,每当有新的键值对要加入到哈希对象时,程序会将保存了键的压缩列表节点推入到压缩列表的表尾,再将保存了值的压缩列表节点推入到压缩列表表尾
    • hashtable编码的哈希对象使用字典作为底层实现,哈希对象中的每个键值都使用一个字典键值来保存对象,字典的每个键值又都是一个字符串对象

    2、当哈希对象同时满足以下两个条件时,列表对象使用ziplist编码,否则将使用hashtable编码:①哈希对象保存的所有键值对的键和值的字符串长度都小于64字节;②列表对象保存的元素数量小于512个(需要注意的是这两个条件是可以通过配置文件中的list-max-ziplist-value和list-max-ziplist-entries进行修改的)

    3、因为哈希键的值为哈希对象,所以用于哈希键的所有命令都是针对哈希对象来构建的,以下为一部分哈希键命令:

    5、集合对象

    1、集合对象的编码可以是intset或者hashtable

    • intset编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合里面
    • hashtable编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,每个字符串对象包含了一个集合元素,而字典的值则全部被设置为null

    2、当集合对象满足哦以下两个条件时,对象使用intset编码,否则使用hashtable编码:①集合对象保存的所有元素都是整数值;②集合对象保存的元素个数不超过512个(第二个条件可以通过配置文件的set-max-intset-entries选项进行修改)

    3、因为集合键的值为集合对象,所以用于集合键的所有命令都是针对集合对象来构建的,以下为一部分集合键命令:

    6、有序集合对象

    1、有序集合对象可以是ziplist或者skiplist

    • ziplist编码的有序集合对象使用压缩列表作为底层实现,每个集合元素使用两个挨在一起的压缩列表节点来保存,第一个节点保存元素的成员(member),第二个节点保存元素的分值(score),分值小的元素会在靠近表头的位置,分值大的元素会在靠近表尾的位置;
    • skiplist编码的有序集合对象使用zset结构作为底层的实现,一个zset结构同时包含一个字典和一个跳跃表;①跳跃表按分值从小到大保存了所有集合元素,每个跳跃表节点都保存了一个集合元素,其中object属性保存了元素的成员,score属性保存了元素的分值,通过跳跃表程序可以对有序集合进行范围型操作;②字典为有序集合创建了一个从成员到分值的映射,字典中的每个键值对都保存了一个集合元素,键保存了元素的成员,值保存了元素的分值,通过字典查找分值操作可以实现O(1)复杂度

    有序集合每个元素都是一个字符串对象,每个元素的分值都是一个double类型的浮点数;zset结构同时使用跳跃表和字典不会产生任何重复成员或分值,因为它们都通过指针来共享相同元素的成员和分值,因而也不会浪费额外的内存

    2、当有序集合对象同时满足以下两个条件时,对象使用ziplist编码,否则将使用skiplist编码:①有序集合保存的元素数量小于128个;②有序集合保存的所有元素成员的长度都小于64字节(需要注意的是这两个条件是可以通过配置文件中的list-max-ziplist-value和list-max-ziplist-entries进行修改的)

    3、有序集合键的值为有序集合对象,所以用于有序集合键的所有命令都是针对有序集合对象来构建的,以下为一部分有序集合键命令:

    7、类型检查与命令多态

    1、Redis中用于操作键的命令基本上分为两种,一种可以对任意类型的键执行,如Del,Expire,Rename,Type,Object命令等;另外一种命令只能针对特定类型的键执行,如下:

    • 字符串键操作:Set、Get、Append、Strlen等命令
    • 哈希键操作:Hdel、Hset、Hget、Hlen等命令
    • 列表键操作:Rpush、Lpop、Linsert、Llen等命令
    • 集合键操作:Sadd、Spop、Sinter、Scard等命令
    • 有序集合键操作:Zadd、Zcard、Zrank、Zscore等命令

    2、执行一个命令前,Redis会先检查输入键的类型是否正确,如不匹配则会返回类型错误;其实现是通过redisObject结构的type属性来实现的

    3、Redis除了会根据值对象的类型来判断键是否能够执行指定命令外,还会根据值对象的编码方式,选择正确的命令实现代码来执行命令;如针对列表对象执行Llen命令,系统会根据列表对象的编码是ziplist还是linkedlist来选择适用的函数进行操作,所以从面向对象的角度看,Llen命令是多态的;

    Del、Expire等命令也是多态的,它们是类型的多态,而像Llen等命令是基于编码的多态

    8、内存回收

    1、C语言是不具备自动内存回收功能的,所以Redis在对象系统中构建了一个引用计数技术实现内存回收机制,通过这一机制,程序可以通过跟踪对象的引用计数信息,来适当的时候自动释放对象并进行内存回收;计数信息由redisObject结构的refcount属性记录

    2、对象的引用计数信息会随着对象的使用状态而不断变化:

    • 创建对象时,引用计数的值会变被初始化为1;
    • 当对象被新程序使用时,引用计数的值会被加1;
    • 当对象不再被一个程序使用时,引用计数的值会被减1;
    • 当对象的引用计数值变为0,对象所占用的内存会被释放;

    3、以下为修改对象引用计数的API

    9、对象共享

    1、对象的引用计数属性还带有对象共享的作用(和.NET的字符串驻留池机制类似);如键A创建了一个包含整数值100的字符串对象,键B同样需要创建一个这样的字符串对象,那么它可以共享键A的字符串对象,而对象的引用计数会从1变为2;共享机制对节约内存非常有帮助,数据库中保存的相同值对象越多,对象共享机制就能节约越多的内存

    2、Redis在初始化服务器时会创建一万个字符串对象,包含从0~9999的整数值,当创建新对象需要用到这些字符串时,服务器就会使用这些共享对象,而不是创建新对象;共享对象不仅只有字符串键可以使用,那些在数据结构中嵌套了字符串对象的对象都可以使用

    10、对象的空转时长

    1、除了type、encoding、ptr、refCount外,redisObject还包含一个lru属性,它记录的是对象最后一次被访问的时间,可以通过Object Idletime命令打印出定键的空转时长,且不会影响lru属性的值

    2、如果服务器打开了maxmemory选项,并且服务器用于回收内存的算法为volatile-lru或allkeys-lru,那么当服务器占用的内存数超过了maxmemory选项设定的上限,空转时间较长的部分键会被服务器优先释放,从而回收内存

  • 相关阅读:
    面试容易问到的Linux问题
    Java面试题复习笔记(框架)
    Java面试题复习笔记(前端)
    Java面试题复习笔记(数据库)
    Java面试题复习笔记(Web方向)
    【刷题-LeetCode】191 Number of 1 Bits
    【数学】随机方法计算逆矩阵
    【刷题-LeetCode】190 Reverse Bits
    【刷题-LeetCode】188 Best Time to Buy and Sell Stock IV
    python 30 面向对象之 多态
  • 原文地址:https://www.cnblogs.com/Jscroop/p/13197886.html
Copyright © 2020-2023  润新知