ICode9

精准搜索请尝试: 精确搜索
首页 > 数据库> 文章详细

《Redis设计与实现》第一部分—数据结构与对象

2020-06-27 12:01:32  阅读:152  来源: 互联网

标签:对象 Redis 保存 集合 哈希 字符串 数据结构 节点


一、简单动态字符串SDS

1、SDS的定义

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

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

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、压缩列表节点的构成

每个压缩列表可以包含一个字节数组或者一个整数值,字节数组可以是以下三种长度之一:①小于等于26-1字节②长度小于等于214-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选项设定的上限,空转时间较长的部分键会被服务器优先释放,从而回收内存

标签:对象,Redis,保存,集合,哈希,字符串,数据结构,节点
来源: https://www.cnblogs.com/Jscroop/p/13197886.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有