Redis设计与实现-6.对象
前面几篇文章,我们一起学习了redis用到的所有主要数据结构,比如简单动态字符串(sds)、双端链表、字典、压缩列表、整数集合等等。
redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个系统包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象,每种对象都用到了至少一种我们前面所介绍的数据结构。
通过这五种这五种不同类型的对象,redis可以在执行命令之前,根据对象的类型来判断一个对象是否可以执行给定的命令。使用对象的另一个好处是,我们可以针对不同的使用场景,为对象设置多种不同的数据结构实现,从而优化对象在不同场景下的使用效率。
除此之外,redis的对象系统还实现了基于引用计数技术的内存回收机制,当程序不再使用某个对象的时候,这个对象所占用的内存就会被自动释放;另外,redis还通过引用计数技术实现了对象共享机制,这一机制可以在适当条件下,通过让多个数据库键共享同一个对象来节约内存。
最后,redis对象带有访问时间记录信息,该信息可以用于计算数据库键的空转时长,在服务器启用了maxmenory功能的情况下,空转时长较大的那些键可能会优先被服务器删除。
接下来我们将逐一学习以上提到的redis对象和特性。
对象的类型与编码
redis使用对象来表示数据库中的键和值,每次当我们在redis 的数据库中新创建一个键值对时,我们至少会创建两个对象,一个对象用作键值对的键,另一个对象用于键值对的值。
reids中的每个对象都由一个redisObject结构表示,该结构中和保存数据有关的三个属性如下
typedef struct redisObject{ //类型 unsigned type:4; //编码 unsigned encoding:4; //指向底层实现数据结构的指针 void *ptr; ……. }robj
类型
对象的type属性记录了对象的类型,这个属性的值是下表中其中的一个。
对于redis数据库保存的键值对键值对俺来说,键总是一个字符串对象,而值则可以是字符串对象、列表对象、哈希对象、集合对象或者有序集合对象其中一种。
所以我们执行type命令时,命令返回的结果为数据库键对应的值对象的类型,而不是键对象的类型。
set msg “hello”
type msg//string
不同值类型所对应的的输出如下
编码和底层实现
对象ptr指针指向对象的底层实现数据结构,而这些数据结构由对象的encoding属性决定。
encoding属性记录了对象所使用的编码,也即是说这个对象使用了什么数据结构作为对象的底层实现,这个属性可以是下面列出的常量的其中之一
每种类型的对象都至少使用了两种不同的编码,每种类型的对象可以使用的编码如下
object encoding 命令可以查看数据库值对象的编码
object encoding msg//“embstr”
下图列出了不同编码对象对应的object encoding输出
通过encoding属性来设定对象所用的编码,而不是为特定类型的对象对象关联一种固定的编码,极大地提升了redis的灵活性和效率,因为redis可以根据不同的使用场景来为一个对象设置不同的编码,从而优化对象在某一场景下的效率。
举个例子,列表对象包含的元素比较少的时候,redis使用压缩列表作为底层实现:
1 因为压缩列表比双端链表更节约内存,并且在元素数量较少时,在内存中以连续块方式保存的压缩列表比起双端链表可以更快被赵茹到缓存中。
2 随着列表对象包含的元素越来越多,使用压缩列表来保存元素的优势逐渐消失时,对象就会将底层实现从压缩列表转向功能更强、更适合保存大量元素的双端链表。
其他类型的对象也会通过使用多种不同的编码来进行类型的优化。
接下来,我们分别学习redis五种不同类型的对象,他们使用的编码方式,转换条件,以及同一个命令在多种不同编码上的实现方法。
字符串对象
字符串对象的编码可以使int,raw或者embstr。
如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性里面(将void *转换成long),并将字符串对象的编码设置为int。
举个例子,执行以下命令
set number 10086
object encoding number //”int”
结构如下图
如果字符串对象保存的是一个字符串值,并且这个字符串值的长度大于32字节,那么字符串对象将使用一个简单动态字符串(sds)来保存这个字符串值,并将对象的编码设置为raw。
如果字符串对象保存的是一个字符串值,并且这个字符串值的长度小于等于32字节,那么字符串对象将使用embstr编码的方式来保存这个字符串值。
embstr编码是专门用于保存短字符串的一种优化编码方式,这种编码和raw编码一样,都使用redisObject结构和sdshdr结构来表示字符串对象,但raw编码会调用两次内存分配函数来分别创建redisObject结构和sdshdr结构,而embstr编码则通过调用一次内存分配函数来分配一块连续的空间,空间中一次包含redisObject和sdshdr连个结构。如下图
embstr编码的字符串对象在执行命令时,产生的效果和raw编码的字符串对象执行命令时产生的效果是相同的,但使用embstr编码的字符串对象来保存短字符串值有以下好处:
1 embstr编码将创建字符串对象所需的内存分配次数从raw编码的两次降为一次。
2 释放embstr编码的字符串对象只需要调用一次内存释放函数,而释放raw编码的字符串对象需要调用两次内存释放函数。
3 因为embstr编码的字符串对象的所有数据都保存在一块连续的内存里面,所以这种编码的字符串对象比起raw编码的字符串对象能够更好地利用缓存带来的优势。
最后要说的是,可以用long double类型表示的浮点数在redis中也是作为字符串值来保存的。如果我们要保存一个浮点数到字符串对象里面,那么程序会先将这个浮点数转换成字符串值,然后再保存转换所得的字符串值。
int编码的字符串对象和embstr编码的字符串对象在条件满足的情况下,会被转换为raw编码的字符串对象。
字符串命令的实现
列表对象
列表对象的编码可以使ziplist或者linkedlist。
ziplist编码的列表对象使用压缩列表作为底层实现,每个压缩列表节点(entry)保存了一个列表元素。下图就是ziplist编码的列表对象,红框内为存储的数据。
另一方面,linkedlist编码的列表对象使用双端链表作为底层实现,每个双端链表节点都保存了一个字符串对象,而每个字符串对象都保存了一个列表元素,如下图
注意,linkedlist编码的列表对象在底层的双端链表结构中包含了多个字符串对象,这种这种嵌套字符串对象的行为在稍后介绍的哈希对象、集合对象和有序集合对象中都会出现,字符串对象是redis五种类型的对象中唯一一种会被其他四中类型对象嵌套对象。
为了简化字符串对象的表示,我们使用StringObject字样来代表字符串对象,完整格式如下
编码转换
当列表对象可以同时满足以下两个条件时,列表对象使用ziplist编码:
1 列表对象保存的所有字符串元素的长度都小于64字节
2 列表对象保存的元素数量小于512个;
(以上两个条件的上限值可以修改)
列表命令的实现
哈希对象
哈希对象的编码可以是ziplist或者hashtable。
ziplist编码的哈希对象使用压缩列表作为底部实现,每当有新的键值对要加入到哈希对象时,程序会先保存了键的压缩列表节点推入到压缩列表表尾,然后再将保存了值的压缩列表节点推入到压缩列表表尾,因此:
1 保存了同一键值对的两个节点总是紧挨在一起,保存键的节点在前,保存值的节点在后
2 先添加到哈希对象中的键值对会被放在压缩列表的表头方向,而后来添加到哈希对象中的键值对会被放在压缩列表的表尾方向。
举个例子,执行如下命令
hset profile name “Tom”
hset profile age 25
hset profile career “Programmer”
那么,他的编码回事ziplist,对应的哈希对象如下图
另一方面,hashtable编码的哈希对象使用字典作为底层实现,哈希对象中的每个键值对都使用一个字典键值对来保存
1 字典的每个键都是一个字符串对象,对象中保存了键值对的键
2 字典的每个值都是一个字符串对象,对象中保存了键值对的值
上例中,对应的hashtable编码的哈希对象如下图
编码转换
当哈希对象可以同时满足一下两个条件时,哈希对象使用ziplist编码
1 哈希对象保存的所有键值对的键和值字符串长度都小于64字节。
2 哈希对象保存的键值对数量小于512个
不能满足这两个条件的哈希对象需要使用hashtable编码(这两个条件的上限值可以在redis配置中修改。)
哈希命令的实现
因为哈希键的值为哈希对象,所以用于哈希键的所有命令都是针对哈希对象来构建的,下表列出一部分哈希键命令,以及这些命令在不同编码对象下的实现方法。
这里就单独说下Hget方法,虽然ziplist编码时,ziplistFind方法复杂度为O(N),但是键值对总数较少,不会超过256,执行速度也是很快的。键值对多的时候,hashtable的实现,复杂度为O(1),执行起来的速度也是很快。
集合对象
集合对象的编码可以是intset或者hashtable。
intset编码的集合对象使用证书集合作为底层实现,集合对象包含的所有元素都被保存在整数集合里面。
另一方面,hashtable编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,每个字符串对象包含了一个集合元素,而字典的值则全部被设置为null。
编码转换
当集合对象可以同时满足一下两个条件时,对象使用intset编码:
1 集合对象保存的所有元素都是整数值
2 集合对象保存的元素数量不超过512个
不能满足这两个条件的集合对象使用hashtable编码。
集合命令的实现
有序集合对象
有序集合的编码可以是ziplist或者skiplist。
ziplist编码的压缩列表对象使用压缩列表作为底层实现,每个集合元素使用两个金爱在一起的压缩列表节点保存,第一个节点保存元素的成员,而第二个元素则保存元素的分值。
压缩列表内的集合元素按分值从小到大金星排序,分值较小的元素被防止在靠近表头的方向,而分值较大的元素责备防止在靠近表尾的方向,如下图。
skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表:
typedef struct zset{ zskiplist *zsl; dict *dict; }zset
zset结构中的zsl跳跃表按分值从小到大保存所有集合元素,每个跳跃表节点都保存了一个集合元素:跳跃表节点的object属性保存了元素的成员,而跳跃表节点的score属性则保存了元素的分值。通过这个跳跃表,程序可以对有序集合进行范围性操作,比如zrank、zrange等命令就是基于跳跃表api来实现的。
除此之外,zset结构中的dict字典为有序集合创建了一个从成员到分值的映射,字典中的每个键值对都保存了一个集合元素:字典的键保存了元素的成员,而字典的值则保存了元素的分值。通过这个字典,程序可以用O(1)复杂度查找给定成员的分值,ZSCORE命令就是根据这一特性实现的,而很多其他有序集合命令都在实现的内部用到了这一特性。
有序集合每个元素的成员都是一个字符串对象,而每个元素的分值都是一个double类型的浮点数。值得一提的是,虽然zset结构同时使用跳跃表和字典来保存有序集合元素,但这两种数据结构都会通过指针来共享相同的成员和分值,所以同时使用跳跃表和字典来保存集合元素不会产生任何重复成员或分值,也不会因此而浪费额外的内存。
编码的转换
当有序集合对象可以同时满足以下两个条件时,对象使用ziplist编码:
1 有序集合同时保持的元素数量小于128个
2 有序集合保存的所有元素成员的长度都小于64字节