《redis设计与实现:第一部分》

第一部分:数据结构与对象

《第一章》

redis的定义redis是一个开源的,高性能的,基于键值对缓存和存储系统通过提供多种键值对数据类型来适应不同场景下的缓存和存储需求同时redis可以胜任消息队列,任务队列等不同的角色。它以字典结构存储数据,允许其他应用通过TCP协议读写字典中的内容。
支持键值数据类型

1.字符串类型(string)

2.散列类型(hash object)

3.列表类型(list object)

4.集合类型(set object)

5.有序集合类型(sorted set object/zset)

redis~消息队列redis可以胜任消息队列,任务队列等不同的角色。
redis~缓存系统redis 因为可以为每个键设置过期时间,过期时间一到,键会自动删除,所以可以作为缓存系统来使用。
redis~实现队列redis 列表类型键可以用来实现队列,并且支持阻塞式读取可以实现一个高性能的优先级队列。同时,redis还支持“发布/订阅”的消息模式,可以基于此构建聊天室等系统。
redis~缓冲区SDS 除了用来保存数据库中的字符串值之外,还被用作缓冲区(buffer):AOF模块中的AOF缓冲区,以及客户端状态中的输入缓冲区,都是SDS实现的。
redis的快速的原因redis数据库中的所有数据都存储在内存中,由于内存的读写速度远快于硬盘,所以redis在性能上对比其他基于硬盘存储的数据库有非常明显的优势redis同时提供对持久化的支持,即可以将内存中的数据异步写入到硬盘中同时不影响继续提供服务
String:

简单动态字符串(simple dynamic string SDS)的抽象类型,并将SDS用作redis的默认字符串表示。

SDS在len 属性中记录了SDS本身的长度,所以获取一个SDS长度的复杂度为o(1)。

设置和更新SDS长度的工作是由SDS的API在执行时自动完成的,SDS无需进行任何手动修改长度的工作。

存储字符串类型:
set stryunlan "hello world"
获取字符串value:
get stryunlan

c 字符串:

以空字符结尾的字符数组。使用长度为n+1的字符数组来标识长度为n的字符串,并且字符串数组的最后一个元素总是空字符'\0'。

c字符串并不记录自身的长度信息,所以获取一个c字符串的长度,程序必须遍历整个字符串,对遇到的每个字符进行计数,直到遇到代表字符串结尾的空字符为止,这个操作复杂度为o(n)。

优点:

空间预分配策略:通过空间预分配策略,redis可以减少连续执行字符串增长操作所需的内存重分配次数。

在扩展SDS空间之前,SDS API 会先检查未使用空间是否足够,如果足够的话,API就会直接使用未使用空间,而无需执行内存重分配。通过这种预分配策略,SDS将连续增长n次字符串所需的内存重分配次数从必定n次降低为最多n次。

惰性空间释放:惰性空间释放用于优化SDS的字符串缩短操作:当SDS的API需要缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待将来使用。

杜绝缓冲区溢出:c字符串获取字符串长度的复杂度高,而且c字符串不记录自身长度带来的另一个问题就是容易造成缓冲区溢出buffer overflow。

C字符串不记录自身长度带来的问题就是容易造成缓冲区溢出。SDS的空间分配策略完全杜绝了发生缓冲区溢出的可能性。当SDS API 需要对SDS进行修改的时候,API 会显检查SDS的空间是否满足修改所需的要求,如不满足,API 会自动将SDS的空间扩展到修改需要的大小,然后才执行实际的修改操作。

减少修改字符串时带来的内存重分配次数。内容拼接(append)忘记check会造成缓冲区溢出。内容截取(trim)会造成内容泄露。

二进制安全:c字符串的字符必须符合某种编码,并且除了字符串的末尾之外,字符串里面不能包含空字符,否则最先被程序读取的空字符将被误认为字符串结尾。这些限制使得c字符串只能保存文本数据,而不能保存图片,音频,视频,压缩文件这样的二进制数据。

SDS的API是二进制安全的。

重点回顾:

1.Redis 只会使用C字符串作为字面量,在大多数情况下,Redis使用简单动态字符串SDS作为字符串表示。

比起C字符串,SDS具有以下优点:

1.常数复杂度获取字符串长度。

2.杜绝缓冲区溢出。

3.减少修改字符串长度时所需的内存重分配次数

4.二进制安全

5.兼容部分C字符串函数。

《第二章:链表》

链表:链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并且可以通过增删节点来灵活的调整链表的长度
链表特定函数

list结构为链表提供了表头指针head,表尾指针tail,以及链表长度计数器len,而dup,free,match成员则是用于实现多态链表所需的类型特定函数:

1.dup 函数用于复制链表节点所保存的值;

2.free函数用于释放链表节点所保存的值;

3.match函数则用于对比链表节点所保存的值和另一个输入值是否相等。

redis 的链表实现的特性

1.双端:链表节点带有prev和next指针,获取某个节点的前置节点和后置节点的复杂度都是O(1)。

2.无环:表头节点的prev和表尾节点的next指针都指向null,对链表的访问以null为终点。

3.带表头指针和表尾指针:通过list结构的head指针和tail指针,程序获取链表的表头节点和表尾节点的复杂度为O(1)。

4.表头长度计数器:程序使用list结构的len属性对list持有的链表节点进行计数,程序获取链表中节点数量的复杂度为O(1)

5.多态:链表节点使用void*指针来保存节点值,并且可以通过list结构的dup,free,match三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。

使用场景:

列表键:列表键底层实现之一就是链表。当一个列表键包含了数量较多的元素,又或者列表中包含的元素都是比较长的字符串时,redis就会使用链表作为列表键的底层实现。

发布|订阅,慢查询,监视器等:发布与订阅,慢查询,监视器等功能都使用到了链表,redis服务器本身还使用链表来保存多个客户端的状态信息,以及使用链表来构建客户端输出缓冲区。

重点回顾:

链表的特点:

1.列表键底层实现之一就是链表;

   发布与订阅,慢查询,监视器等功能都使用到了链表

2.每个链表节点由一个listNode结构来表示,每个节点都有一个指向前置节点和后置节点的指针,所以redis的链表实现是双端链表。

3.每个链表使用一个list结构来表示,这个结构带有表头节点指针,表尾节点指针,以及链表长度等信息。

4.因为链表头节点的前置节点和表尾节点的后置节点都指向null,所以redis的链表实现是无环链表。

5.通过为链表设置不同的类型特定函数,redis的链表可以用于保存各种不同类型的值。

《第四章:字典》

字典:又称符号表,关联数组或者映射,是一种用于保存键值对的抽象数据结构。在字典中,一个键key可以和一个值value进行关联,这些关联的键和值就称为键值对。
字典的特点:字典中每个键都是独一无二的,程序可以在字典中根据查找与关联的值,或者通过键来更新值,又或者根据键来删除整个键值对。
字典的实现:redis的字典使用哈希表作为底层实现,一个哈希表里面可以又多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对。
哈希算法:

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

总结:

1.当字典被用作数据库的底层实现,或者哈希键的底层实现时,Redis使用MurmurHash2算法来计算的哈希值。

2.优点:即使输入的键是有规律的,算法仍然可以给出一个很少的随机分布性,并且算法的计算速度也非常快。

注意事项:

哈希冲突:当有两个或以上数量的键被分配到了哈希表数组的同一个索引上面时,称hash键冲突。

解决键冲突:redis的哈希表使用链地址法来解决键冲突,每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表链接起来,这就解决了键冲突的问题。

rehash原因:随着操作的不断执行,哈希表保存的键值对会逐渐的增多或者减少,为了让哈希表的负载因子(load factor)维持在一个合理的范围之内,当哈希表保存的键值对数量太多或者太少时,程序需要对哈希表的大小进行相应的扩展或者收缩。扩展和收缩哈希表的工作可以通过执行rehash(重新散列)操作来完成。

哈希表的扩展和收缩:

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

1.服务器目前没有在指定bgsave命令或者bgrewriteaof命令,并且哈希表的负载因子大于等于1。

2.服务器目前正在指定bgsave命令或者bgrewriteaof 命令,并且哈希表的负载因子大于等于5。

redis 对字典的哈希表执行rehash的步骤如下:

1.为字典的ht[1]哈希表分配空间,这个哈希表的空间大小要取决于要执行的操作,以及ht[0]当前包含的键值对数量:

2.将保存在ht[0]的所有键值对rehash到ht[1]上面:rehash指的是重新计算键的哈希值和索引值,然后将键值对放置到ht[1]哈希表的指定位置上。

3.当ht[0]包含的所有键值对都迁移到了ht[1]之后,释放ht[0],将ht[1]设置为ht[0],并在ht[1]新创建一个空白哈希表,为下一次rehash做准备。

哈希表渐进式rehash的详细步骤:

1.为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表。

2.在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash工作正式开始。

3.在rehash进行期间,每次对字典执行增删改查时,程序除了执行指定的操作以外,还会顺带将ht[0]哈希表在rehashidx索引上的所有值对rehash到ht[1],当rehash工作完成之后,程序将rehashidx属性的值增一。

4.随着字典操作的不断执行,最终在某个时间点上,ht[0]的所有键值对都会被rehash到ht[1],这时程序将rehashidx属性的值设为-1,表示rehash操作已完成。

总结:渐进式rehash的好处是它采用分而治之的方式,将rehash键值对所需的计算工作均摊到堆字典的每个添加,删除,查找,更新上,从而避免了集中式rehash而带来的庞大计算量。

重点:根据bgsave命令或bgrewriteaop命令是否正在执行,服务器执行扩展操作所需的负载因子并不相同,这是因为在执行bgsave命令或bgrewriteaop命令的过程中redis需要创建当前服务器进程的子进程,而大多数操作系统都采用写时复制(copy-on-write)技术来优化子进程的使用效率,所以在子进程存在期间,服务器会提高执行扩展操作所需的负载因子,从而尽可能的避免在子进程存在期间进行哈希表扩展操作,这可以避免不必要的内存写入操作,最大限度的节约内存。
渐进式rehash:

rehash动作不是一次性,集中式的完成,而是分多次,渐进式完成的。

多次rehash的原因:如果数据量很多的时候,从ht[0]rehash到ht[1]时,庞大的计算量会导致服务器在一段时间内停止服务。

使用场景

1.数据库的实现:redis的数据库是使用字典来作为底层实现对数据库的增删改查操作也是构建在字典的操作上的。

2.哈希键:字典是哈希键的底层实现之一,当一个哈希键包含的键值对比较多,又或者键值对中的元素都是比较长的字符串时,redis就会使用字典作为哈希键的底层实现。

重点回顾:

1.字典被用于实现redis的各种功能,包括数据库和哈希键。

2.redis中的字典使用哈希表作为底层实现,每个字典带有两个哈希表,一个平时使用,另外一个仅在进行ReHash时使用。

3.当字典被用作数据库的底层实现,或者哈希键的底层实现时,redis使用MurmurHash2算法来计算键的哈希值。

4.哈希表使用链地址法来解决键冲突被分配到同一个索引上的多个键值对会连接成一个单向链表

5.在对哈希表进行扩展或者收缩操作时,程序需要将现有哈希表包含的所有键值对rehash到新哈希表里面,并且这个rehash过程并不是一次性完成的,而是渐进式的完成的。

《第五章:跳跃表》

跳跃表:

跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。

redis 的跳跃表实现由redis.h/zskiplistNode和redis.h/zskiplist两个结构定义。其中zskiplist用于保存跳跃表信息(表头节点,表尾节点,长度),而zskiplistNode则用于表示跳跃表节点。

跳跃表的特点:

跳跃表支持平均O(logN),最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。在大部分情况下,跳跃表的效率可以和平衡树相媲美,并且因为跳跃表的实现比平衡树要简单,所以有不少程序都是用跳跃表代替平衡树。

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

跳跃表包属性有:

header:指向跳跃表的表头节点。

tail:指向跳跃表的表尾节点。

level:记录目前跳跃表内,层数最大的那个节点的层数。

length:记录跳跃表的长度,既跳跃表目前包含节点的数量。

使用场景

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

2.集群节点中用作内部数据结构:redis中,跳跃表只用到了两个地方,一个是实现有序集合键,一个是集群节点中用作内部数据结构。

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

1.层level:节点中用L1,L2,L3等字样标记节点的各个层。每个层有两个属性:前进指针和跨度。前进指针用于访问位于表尾方向的其他节点,而跨度则记录了前进指针所指向节点和当前节点的距离。

2.后退指针backward:节点中用BW字样标记节点的后退指针,它指向位于当前节点的前一个节点。后退指针在程序从表尾向表头遍历时使用。

3.分值score:各个节点中的1.0,2.0,3.0是节点所保存的分值,在跳跃表中,节点按各自所保存的分值从小到大排列。

4.成员对象object:各个节点中的o1,o2,o3是节点所保存的成员对象。

名词解释:

层:跳跃表节点的level数组可以包含多个元素,每个元素都包含一个指向其他节点的指针,程序可以通过这些层来加快访问其他节点的速度,一般来说,层的数量越多,访问其他节点的速度越快。

每次创建一个新跳跃表节点的时候,程序都根据幂次定律(power law ,越大的数出现的概率越小)随机生成一个介于1和32之间的值作为level数组的大小,这个大小就是层的“高度”。

前进指针:每个层都有一个指向表尾方向的前进指针,用于从表头向表尾方向访问节点。

跨度:

层的跨度用于记录两个节点之间的距离:


1.两个节点之间的跨度越大,他们相距的就越远。


2.指向null的所有前进指针的跨度都为0,因为它们没有连向任何节点。

后退指针:节点的后退指针用于从表尾向表头方向访问节点:跟可以一次跳过多个节点的前进指针不同,因为每个节点只有一个后退指针,所以每次只能后退至前一个节点。

分值和成员:节点的分值score属性是一个double类型的浮点数,跳跃表中的所有节点都按分值从小到大来排序。

......

1.节点的成员对象objec属性是一个指针,它指向一个字符串对象,而字符串对象则保存着一个SDS值。

2.每个跳跃表节点的层高都是1至32之间的随机数。

3.在同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的成员对象必须是惟一的。

4.跳跃表中的节点按照分值大小进行排序,当分值相同时,节点按照成员对象的大小进行排序。

靠多个跳跃表节点可以组成一个跳跃表。通过使用一个zskiplist结构来持有这些节点,程序可以更方便的对整个跳跃表进行处理,比如快速访问跳跃表的表头节点和表尾节点,或者快速的获取跳跃表节点的数量等信息。

header和tail 指针分别指向跳跃表的表头和表尾节点,通过这两个指针,程序定位表头节点和表尾节点的复杂度伟O1(1)。

通过使用length属性来记录节点的数量,程序可以在O(1)复杂度内返回跳跃表的长度。level属性则用于在O(1)复杂度内获取跳跃表中层高最大的那个节点的层数量,注意表头节点的层高并不在计算在内。

重点回顾:

1.跳跃表是有序集合的底层实现之一。

2.redis的跳跃表实现zskiplist和zskiplistNode两个结构组成,其中zskiplist用于保存跳跃表信息(表头节点,表尾节点,长度),而zskiplistNode则用于表示跳跃表节点。

3.每个跳跃表节点的层高都是1到32之间的随机数。

4.在同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的成员对象必须是唯一的。

5.跳跃表中的节点按照分值大小进行排序,当分值相同时,节点按照成员对象的大小进行排序。

《第六章:整数集合》

整数集合:是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,redis就会使用整数集合作为集合键的底层实现。
整数集合的实现:整数集合是redis用于保存整数值的集合抽象数据结构,它可以保存类型为int16_t,int32_t,int64_t的整数值,并且保证集合中不会出现重复元素。
contents 数组是整数集合的底层实现:整数集合的每个元素都是contents数组的一个数据项(item),各个项在数组中按值的大小从小到大有序的排列,并且数组中不包含任何重复项。
length属性length属性记录了整数集合包含的元素数量,也即是contents数组的长度。
升级:

升级的原因:每当我们要将一个新元素添加到整数集合里面,并且新元素的类型比整数集合现有元素的类型都要长时,整数集合需要先进行升级,然后才能将新元素添加到整数集合里面。

升级整数集合并添加新元素共分为三步进行:

1.根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间。

2.将底层数组现有的所有元素都转换成与新元素相同的类型,并将类型转换后的元素放置到正确的位上,而且在放置元素的过程中,需要继续维持底层数组的有序性质不变。

3.将新元素添加到底层数组里面。

升级的好处:

1.提升灵活度:因为c语言是静态类型语言,为了避免类型错误,我们不会将两种不同类型的值放在同一个数据结构里面。

2.节约内存:整数集合的做法是可以让集合能同时保存三种不同类型的值,又可以确保升级操作只会在有需要的时候进行,这可以尽量节省内存。

降级:整数集合不支持降级操作,一旦对数组进行了升级,编码就会一直保持升级后的状态。(也就是不支持降级操作)
重点回顾:

1.整数集合是集合键的底层实现之一。

2.整数集合的底层实现为数组,这个数组以有序,无重复的方式保存集合元素,在有需要时,程序会根据新添加元素的类型,改变这个数组的类型。

3.升级操作为整数集合带来了操作上的灵活性,并且尽可能的节约了内存。

4.整数集合只支持升级操作,不支持降级操作。

《第七章:压缩列表》

压缩列表压缩列表是redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构。一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。
连锁更新:

redis将这种在特殊情况下产生的连续多次空间扩展操作称为“连锁更新”(cascade update);

连锁更新在最坏情况下需要对压缩列表执行n次空间重分配操作,而每次空间重分配的最坏复杂度为o(n),所以连锁更新的最坏复杂度为o(n2)。

连锁更新的复杂度较高,但它造成性能问题的几率是很低的:

1.压缩列表里要恰好有多个连续的,长度介于250字节到253个字节之间的节点,连锁更新才有可能被引发,在实际中,这种情况并不多见。

2.即使出现连锁更新,但只要被更新的节点数量不多,就不会对性能造成任何影响。

......

encoding:节点的encoding 属性记录了节点的content 属性所保存数据的类型以及长度。

content:节点的content 属性负责保存节点的值,节点值可以是一个字节数组或者整数,值的类型和长度又节点的encoding属性决定。

使用场景:

压缩列表(ziplist) 是列表键和哈希键的底层实现之一

列表键:当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么redis就会使用压缩列表来做列表键的底层实现。

哈希键:当一个哈希键只包含少量键值对,且每个键值对的键和值要么就是小整数值,要么就是长度比较短的字符串,那么redis就会使用压缩列表来做哈希键的底层实现。

重点回顾:

1.压缩列表压缩列表是redis 为节约内存而开发的顺序型数据结构。

2.压缩列表被用作列表键和哈希键的底层实现之一。

3.压缩列表可以包含多个节点,每个节点可以保存一个字节数组或者整数值。

4.添加新节点到压缩列表,或者从压缩列表中删除节点,可能会引发连锁更新操作,但是这种操作出现的几率不高。

《第八章:对象》

对象:

redis 基于简单动态字符串,双端链表,字典,压缩列表,整数集合等数据结构创建了一个对象系统,这个系统包含字符串对象,列表对象,哈希对象,集合对象,有序集合对象这五种类型的对象。

redis 的对象系统还实现了基于引用计数技术的内存回收机制,当程序不再使用某个对象的时候,这个对象所占用的内存就会被自动释放。redis 还通过引用计数技术实现了对象共享机制,这一机制可以在适当的条件下,通过让多个数据库键共享同一个对象来节约内存。redis的对象带有访问时间记录信息,该信息可以用于计算数据库键的空转时长,在服务器启用了Max memory 功能的情况下,空转时长较大的那些键可能会优先被服务器删除。

对象的类型与编码:redis 使用对象来表示数据库中的键和值,每次我们在redis的数据库中新创建一个键值对时,我们至少会创建两个对象,一个对象用作键值对的键,另一个对象用作键值对的值。
编码和底层实现:

对象的ptr指针指向对象的底层实现数据结构,而这些数据结构由对象的encoding属性决定。

属性记录了对象所使用的编码,也就是说这个对象使用了什么数据结构作为对象的底层实现。

encoding

通过encoding 属性来设定对象所使用的编码,而不是为特定类型的对象关联一种固定的编码,极大的提升了redis的灵活性和效率,因为redis可以根据不同的使用场景来为一个对象设置不同的编码,从而优化对象在某一场景下的效率。

举例:在列表对象包含的元素较少时,redis使用压缩列表作为列表对象的底层实现:

1.因为压缩列表比双端链表更节约内存,并且在元素数量较少时,在内存中以连续块方式保存的压缩列表比起双端链表可以更快的被载入到缓存中。

2.随着列表对象包含的元素越来越多,使用压缩列表来保存元素的优势慢慢消失,对象就会将底层实现从压缩列表转向功能更强,也更适合保存大量元素的双端链表上面。

字符串对象

字符串对象的编码可以时int,raw,embstr。

如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性里面,并将字符串对象的编码设置为int。

set msg "hello wrold"

object encoding msg

set story "long long long long long long long age..."

object encoding story

sadd numbers 1 3 5

object encoding numbers

sadd numbers "seven"

object encoding numbers

set number 10086

object encoding number

【如果字符串对象保存的是一个字符串,并且这个字符串的长度大于32字节,那么字符串对象将使用SDS来保存这个字符串值,并将对象的编码设置为raw;如果小于32字节,那么字符串对象将使用embstr编码的方式来保存这个字符串值。】。

embstr

embstr 编码是专门用于保存短字符串的一种优化编码方式。
 

如果字符串对象保存的是一个字符串值,并且这个字符串值的长度小于等于32字节,那么字符串对象将使用embstr编码的方式来保存这个字符串值。

embstr编码是专门用于保存短字符串的一种优化编码方式,这种编码方式和raw编码一样,都使用redisObject结构和sdshdr结构来表示字符串对象,但raw编码会调用两次内存分配函数分别创建redisObject结构和sdshdr结构,而embstr编码则通过调用一次内存分配函数分配一块连续的空间。空间中依次包含redisObject结构和sdshdr结构。

使用embstr编码的字符串对象来保存短字符串值有以下好处:

1.embstr 编码将创建字符串对象所需的内存分配次数从raw 编码的两次降低为一次。

2.释放embstr 编码的字符串对象只需要调用一次内存释放函数,而释放raw编码的字符串对象需要调用两次内存释放函数。

3.因为embstr 编码的字符串对象的所有数据都保存在一块连续的内存里面,所以这种编码的字符串对象比起raw编码的字符串对象能够更好的利用缓存带来的好处。

编码的转换:int 编码的字符串对象和embstr 编码的字符串对象在条件满足的情况下,会被转换为raw编码的字符串对象。

列表对象:

列表对象的编码可以是ziplist或者linkedlist。ziplist 编码的列表对象使用压缩列表作为底层实现,每个压缩列表节点(entry)保存了一个列表元素。

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

linkedList:linkedList编码的列表对象使用双端链表作为底层实现,每个双端链表节点node都保存了一个字符串对象,而每个字符串对象都保存了一个列表元素。

编码转换:当列表对象可以同时满足以下两个条件时,列表对象使用ziplist 编码:

1.列表对象保存的所有字符串元素的长度都小于64字节。 2.列表对象保存的元素数量小于512个;

总结:不能满足这两个条件的列表对象需要使用linkedlist编码。

linkedList编码的列表对象在底层的双端链表结构中包含了多个字符串对象,字符串对象是redis五种类型的对象中唯一一种会被其他四种类型对象嵌套的对象。 

对于使用ziplist编码的列表对象来讲,当使用ziplist编码所需的两个条件的任意一个不能被满足时,对象的编码转换操作就会被执行,原本保存在压缩列表里的所有列表元素都会被转移并保存到双端链表里面,对象的编码也会从ziplist 变成linkedlist。

列表键的值为列表对象,所以用户列表键的所有命令都是针对列表对象来构建的。

哈希对象

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

ziplist 编码的哈希对象使用压缩列表作为底层实现,每当有新的键值对要加入到哈希对象时,程序会先将保存了键的压缩列表节点推入到压缩列表表尾,然后再将保存了值的压缩列表节点推入到压缩列表表尾,因此:

1.保存了同一键值对的两个节点总是紧挨在一起,保存键的节点在前,保存值的节点在后。

2.先添加到哈希对象中的键值对会被放在压缩列表的表头方向,而后来添加到哈希对象中的键值对会被放在压缩列表的表尾方向。

hashtable 编码的哈希对象使用字典作为底层实现,哈希对象中的每个键值对都是用一个字典键值对来保存:

1.字典中的每个键都是一个字符串对象,对象中保存了键值对的键;

2.字典中的每个值都是一个字符串对象,对象中保存了键值对的值。

编码转换:当哈希对象可以同时满足以下两个条件时,哈希对象使用ziplist编码:

1.哈希对象保存的所有键值对的键和值的字符串长度都小于64字节;

2.哈希对象保存的键值对数量小于512个;

总结:不能满足这两个条件的哈希对象需要使用hashtable 编码。

集合对象

集合对象的编码可以时intset 或者hashtable。

intset 编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合里面。

hashtable编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,每个字符串对象包含了一个集合元素,而字典的值则全部被设置为NULL;

编码的转换: 当集合对象可以同时满足以下两个条件时,对象使用intset编码:

1.集合对象保存的所有元素都是整数值

2.集合对象保存的元素数量不超过512个

总结:不能满足这两个条件的集合对象需要使用hashtable 编码。

对于使用intset编码的集合对象来说,当使用intset编码所需的两个条件的任意一个不能被满足时,就会执行对象的之编码转换操作,原本保存在整数集合中的所有元素都会被转移并保存到字典里面,并且对象的编码也会从intset变为hashtable。

有序集合对象:

有序集合的编码可以是ziplist 或者skiplist。

ziplist 编码的压缩列表对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,第二个元素则保存元素的分值score。

压缩列表内的集合元素按分值从小到大进行排序,分值较小的元素被放置在 靠近表头的方向,而分值较大的元素被放置在靠近表尾的方向。

编码的转换:当有序集合对象可以同时满足以下两个条件时,对象使用ziplist 编码:

1.有序集合保存的元素数量小于128个

2.有序集合保存的所有元素成员的长度都小于64字节。

总结:不能满足以上两个条件的有序集合对象将使用skiplist编码。

对于使用ziplist编码的有序集合对象来说,当使用ziplist编码所需的两个条件中的任意一个不能被满足时,就会执行对象的编码转换操作,原本保存在压缩列表里的所有集合元素都会被转移并保存到zset结构里面,对象的编码也会从ziplist变成zkiplist。

有序集合命令的实现:因为有序集合键的值为哈希对象,所以用于有序集合键的所有命令都是针对哈希对象来构建的。

类型检查与命令多态:redis 中用于操作键的命令基本上可以分为两种类型。其中一种命令可以堆任何类型的键执行,比如说del命令,expire命令,rename命令,type命令,object命令。

而另一种命令只能对特定类型的键执行,比如说:

命令只能对特定类型的键执行
1.set,get,append,strlen 等命令只能对字符串键执行。
2.hdel,hset,hget,hlen等命令只能对哈希键执行
3.rpush,lpop,linsert,llen命令只能对列表键执行
4.sadd,spop,sinter,scard 命令只能对集合键执行
5.zadd ,zcard, zrank,zscore 命令只能对有序集合键执行。

内存回收:因为c语言不具备自动内存回收功能,所以redis 在自己的对象系统中构建了一个引用计数(reference counting)技术实现的内存回收机制,通过这一机制,程序可以通过跟踪对象的引用计数信息,在适当的时候自动释放对象并进行内存回收。

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

1.在创建一个新对象时,引用计数的值会被初始化为1;
2.当对象被一个新程序使用时,它的引用计数值会被+1;
3.当对象不再被一个程序使用时,它的引用计数值会被-1;
4.当对象的引用计数值变为0,对象所占用的内存会被释放。
对象共享:引用计数除了用于实现内存回收机制,对象的引用计数属性还带有对象共享的作用。
对象的整个生命周期:创建对象,操作对象,释放对象。

redis中,让多个键共享同一个值对象需要执行以下两个步骤:

1.将数据库键的值指针指向一个现有的值对象。
2.将被共享的值对象的引用计数增一。

重点回顾:

1.redis 数据库中的每个键值对的键和值都是一个对象。
2.redis 共有字符串,列表,哈希,集合,有序集合五种类型的对象,每种类型的对象至少都有两种或以上的编码方式,不同的编码可以在不同的使用场景上优化对象的使用效率。
3.服务器在执行某些命令前,会先检查给定键的类型能否执行指定的命令,而检查一个键的类型就是检查键的值对象的类型。
4.redis 的对象系统带有引用计数实现的内存回收机制,当一个对象不再被使用时,该对象所占用的内存就会被自动释放。
5.redis 会共享值为0到9999的字符串对象
6.对象会记录自己的最后一次被访问的时间,这个时间可以用于计算对象的空转空间。

 下一章:《redis设计与实现:第二部分》_welcome to 一点点 home-CSDN博客

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值