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

Reids并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个系统包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象,每种对象至少一种数据结构。
Redis根据对象的类型来判断对象是否可以执行给定的命令。使用对象可以针对不同的场景为对象设置多种不同的数据结构实现,从而优化对象在不同场景下的使用效率。
Redis的对象系统还实现了基于引用计数的内存回收机制,当程序不再使用某个对象的时候,这个对象所占用的内存就会被自动释放,另外redis还通过引用计数技术实现了对象共享机制。另外对象还带有访问时间记录信息,该信息在服务器启用了maxmemory功能的情况系啊,空转时长较大的那些键可能会优化被服务器删除。

对象的类型与编码
Redis使用对象来表示数据库中的键和值,每次当我们在redis的数据库中新创建一个键值对时,我们至少会创建两个对象,一个对象用作键值对的键(键对象),另一个对象用作键值对的值(值对象)。
Redis中的每个对象都由一个redisObject结构表示,该结构中和保存数据有关的三个属性分别是type属性,encoding属性和ptr属性:
typedef struct redisObject {
//类型
unsigned type:4;
//编码
unsigned encoding:4;
//指向底层实现的数据结构的指针
void *prt;
// …
} robj;

类型
键的类型总是一个字符串对象
值的类型可以表8-1列出的常量的其中一个。
在这里插入图片描述

TYPE命令返回的也是值对象的类型,而不是键对象的类型。
在这里插入图片描述

编码和底层实现
对象的ptr指针指向对象的底层实现数据结构,而这些数据结构由对象的encoding属性决定。encoding属性记录了对象所使用的编码,也即是说这个对象使用了什么数据结构作为对象的底层实现,这个属性的值可以是表8-3列出的常量中的一个。
在这里插入图片描述

每种类型的对象都至少使用了两种不同的编码来实现,表8-4列出了每种类型的对象可以使用的编码。
在这里插入图片描述

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

举个例子,列表对象包含的元素比较少时,Redis使用压缩列表作为对象的底层实现:
1.因为压缩列表比双端链表节约内存,并且在元素数量较少时,在内存中以连续块的方式保存的压缩列表比起双端链表可以更快载入内存
2.随着列表对象包含的元素越来越多,使用压缩列表来保存元素的优势逐渐消失时,对象就会将底层实现从压缩列表转向功能更强、也更适合保存大量元素的双端链表上面。

字符串对象
值 编码
可以用long类型保存的整数 int
可以用long double类型保存的浮点数 embstr或raw
字符串值,或者因为长度太大而没办法用long类型表示的整数,又或者因为长度太大而没办法用long double类型表示的浮点数 embstr或者raw
字符串值,并且长度大于32字节 raw
字符串值,长度小于等于32字节 embstr

embstr编码是专门用于保存短字符串的一种优化编码方式,这种编码和raw编码一样,都使用redisObject结构和sdshdr结构来表示字符串对象,但使用embstr编码的字符串对象来保存短字符串有以下好处:
1.embstr编码将创建字符串对象所需的内存分配次数从raw的两次降低到一次。
2.释放embstr编码的字符串对象只需要调用一次内存释放函数,而raw需要两次
3.因为embstr编码的字符串对象的所有数据结构都保存在一块连续的内存里面,所以这种编码的字符串对象比起raw编码的字符串对象能够更好的利用缓存带来的优势。

编码的转换
int编码和embstr编码的字符串对象在条件满足的情况下,会被转换为raw编码的字符串对象。
embstr编码是只读的,int编码和raw可以修改,所以embstr在修改命令执行之后就变成了一个raw编码的字符串对象。

字符串命令的实现
在这里插入图片描述

列表对象
列表对象的编码可以是ziplist或者linkedlist。
在这里插入图片描述
在这里插入图片描述

编码转换
当列表对象可以同时满足以下两个条件时,列表对象使用ziplist编码:
1.列表对象保存的所有字符串元素的长度都小于64字节,
2.列表对象保存的元素数量小于512个
其它使用linkedlist编码

注意
以上两个条件的上限值是可以修改的,具体请看配置文件中关于list-max-ziplist-value选项和list-max-ziplist-entries选项的说明。

列表命令的实现
在这里插入图片描述

哈希对象
哈希对象的编码可以是ziplist或者hashtable。
ziplist编码的哈希对象使用压缩列表作为底层实现,每当有新的键值对要加入到哈希对象时,程序会将保存了键的压缩列表节点推入到压缩列表表尾,然后再将保存了值的压缩列表节点推入到压缩列表表尾。因此:1.保存了同一键值对的两个节点总是紧挨在一起,保存键的节点在前,保存值的节点在后;2.先添加到哈希对象中的键值对会放在压缩列表的表头方向,而后来添加到哈希对象中的键值对会被放在压缩列表的表尾方向。

在这里插入图片描述
在这里插入图片描述

hashtable编码的哈希对象使用字典作为底层实现,哈希对象中的每个键值对都使用一个字典键值对来保存。
另一方面,hashtable编码的哈希对象使用字典作为底层实现,哈希对象中的每个键值对都使用一个字典键值对来保存:1.字典的每个键都是一个字符串对象,对象中保存了键值对的键;2.字典的每个值都是一个字符串对象,对象中保存了键值对的值。
在这里插入图片描述

编码转换:
当列表对象可以同时满足以下两个条件时,列表对象使用ziplist编码:
1.列表对象保存的所有字符串元素的长度都小于64字节,
2.列表对象保存的元素数量小于512个
其它使用hashtable编码
注意
以上两个条件的上限值是可以修改的,具体请看配置文件中关于hash-max-ziplist-value选项和hash-max-ziplist-entries选项的说明。

哈希命令的实现
在这里插入图片描述

集合对象
集合对象的编码可以是intset或者hashtable。
intset编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合里面。另一方面,hashtable编码的集合对象使用字典作为底层实现,字典的每一个键都是一个字符串对象,每个字符串对象包含了一个集合元素,而字典的值则全部被设置为NULL。
在这里插入图片描述

编码转换
当列表对象可以同时满足以下两个条件时,列表对象使用intset编码:
1.列表对象保存的所有元素的都是整数值,
2.列表对象保存的元素数量小于512个
其它使用hashtable编码
注意
以上两个条件的上限值是可以修改的,具体请看配置文件中关于set-max-inset-entries选项的说明。
在这里插入图片描述
在这里插入图片描述

有序集合对象
有序集合的编码可以是ziplist或者skiplist。
ziplist编码的压缩列表对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员(member),而第二个节点则保存元素的分值(score)。
压缩列表内的集合元素则按分值从小到大进行排序,分值较小的元素被放置在靠近表头的方向,而分值较大的元素则被放置在靠近表尾的方向。
typedef struct zset {
zskiplist *zsl;
dict *dict;
}

在这里插入图片描述
在这里插入图片描述

为什么有序集合需要同时使用跳跃表和字典实现?
在理论上,有序集合可以单独使用字典或者跳跃表的其中一种数据结构来实现,但无论单独使用字典还是跳跃表,在性能上对比起同时使用字典和跳跃表都会有所降低。单独使用跳跃表,O(1)复杂度查找成员的分值这一特性就没了,单独使用字典,O(1)范围查询的特性就没了。
有序集合的每个元素的成员都是一个字符串对象,而每个元素的分值都是一个double类型的浮点数。值得一提的是,虽然zset结构同时使用跳跃表和字典来保存有序集合元素,但这两种数据结构都会通过指针来共享相同元素的成员和分值,所以同时使用跳跃表和字典来保存集合元素不会产生任何重复成员或者分值,也不会因此而浪费额外的内存。
在这里插入图片描述

编码转换:
当列表对象可以同时满足以下两个条件时,列表对象使用ziplist编码:
1.列表对象保存的所有字符串元素的长度都小于64字节,
2.列表对象保存的元素数量小于512个
其它使用hashtable编码
注意
以上两个条件的上限值是可以修改的,具体请看配置文件中关于zset-max-ziplist-value选项和zset-max-ziplist-entries选项的说明。

有序集合命令的实现
在这里插入图片描述

类型检查与命令多态
Redis中用于操作键的命令基本上可以分为两种类型。一种是可以操作任何类型的键执行,比如说DEL、EXPIRE、RENAME、TYPE、OBJECT命令;另一种是只能对特定类型的键执行,比如说:
在这里插入图片描述

类型检查的实现
在执行一个类型特定的命令之前,Redis会检查输入键的类型是否正确,然后再决定是否执行给定的命令。类型特定命令所进行的类型检查是通过redisObject结构的type属性来实现的:在执行一个类型特定命令之前,服务器会先检查输入数据库键的值对象是否为执行命令所需的类型,如果是的,服务器就对键执行特定的命令;否则,服务器拒绝执行命令。

多态命令的实现
Redis除了会根据值对象的类型来判断键是否能够执行指定命令之外,还会根据值对象的编码方式,选择正确的命令实现代码来执行命令。DEL、EXPIRE、TYPE等命令也称为基于类型的多态(一个命令可以同时用于处理多种不同类型的键),而后者是基于编码的多态(一个命令可以同时用于处理多种不同的编码)。
在这里插入图片描述

内存回收
c语言不具备自动内存回收功能,所以Redis在自己的对象系统中构建了一个引用计数器(reference conunting)技术实现内存回收机制,通过这一机制,程序可以通过跟踪对象的引用技术信息,在适当的时候自动释放对象并进行内存回收。
typedef struct redisObject {
//引用计数
int refcount;
} robj;
在这里插入图片描述

对象共享
除了用于实现引用计数内存回收机制之外,对象的引用技术属性还带有对象共享的作用。
让多个键共享同一个值对象需要执行以下两个步骤:
1.将数据库键的指针指向同一个现有的值对象
2.将被共享的值对象的引用计数增一
共享的越多,节省的内存越多。
Redis会在初始化服务器的时候,创建了一万个字符串对象,这些对象包含了从0到9999的所有整数值,服务器需要去哦用到0-9999的字符串对象时,会共享对象,而不是新创建对象。
注意: 创建共享字符串对象的数量可以通过修改redis.h/REDIS_SHARED_INTEGERS常量来修改

set A 100 有一个共享 set B 100 有两个共享
在这里插入图片描述

尽管共享更复杂的对象可以节约更多的内存,但受到CPU时间的限制,Redis只对包含整数值的字符串对象进行共享。

对象的空转时长
redisObject结构包含的最后一个属性为lru属性,该属性记录了对象最后一次被程序访问的时间:
typedef struct redisObject {
unsigned lru:22;
} robj;
OBJECT IDLETIME命令可以打印出给定键的空转时长。这一空转时长就是通过将当前时间减去键的值对象的lru时间计算得出的。
如果服务器打开了maxmemory选项,并且服务器用于回收内存的算法为volatile-lru或者allkeys-lru,那么当服务器占用的内存数超过了maxmemory选项设置的上限值是,空转时长较高的部分键会优先被服务器释放,从而回收内存。

在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章