上篇博客提到过redis里面涉及到 字符串对象、列表对象、哈希对象、集合对象和有序对象 这五种对象类型。下面将分别对这五个对象做进一步了解。
redis对象都是由一个redisObject结构来表示的,该结构如下:
类型type包括以下五种:
编码encoding记录了对象所使用的编码,涉及到的编码如下图所示:
redis中每个类型的对象最少使用两种不同的编码,下图列出了每种对象可以使用编码的情况:
下面将逐一介绍五种对象类型。
1)字符串对象
字符串对象可以使用的编码是int,raw或者embstr。下图列出了字符串对象保存各种不同类型的值所使用的编码方式。
int编码的字符串对象和embstr编码的字符串对象在条件满足的情况下,会被转换为raw对象。
对于int编码的字符串对象来说,当我们向对象执行一些命令,使得保存的不在是整数值,那么int编码就会变成raw编码。
redis没有为embstr编码的字符串对象编写任何修改的程序,所以embstr编码的字符串对象实际上只是可读的。当我们对其进 行修改操作时,那么embstr编码就会变成raw编码。
2)列表对象
列表对象的编码可以是ziplist或者linkedlist。
ziplist编码的列表对象使用压缩列表作为底层实现,
另一方面,linkedlist编码的列表对象使用双端列表作为底层实现,每个列表节点都保存了一个字符对象,每个字符对象都保存了一个列表元素,如下图举例(字符对象下图用StringObject简化表示):
备注:字符串对象是redis中五种类型的对象中唯一一种会被其他四种类型对象嵌套的对象。
编码转换:当列表对象同时满足以下两个条件时才会使用ziplist,其他情况均会使用linkedlist。
①列表对象保存的所有字符串元素的长度都小于64字节;
②列表对象保存的元素数量小于512个;(以上两个值均可以通过配置文件修改相应参数)
对于使用ziplist编码的列表对象有以上两个条件不被满足时,编码转换就会发生,所有对象的编码就会变成linkedlist编码,原先保存在压缩列表的元素也会转移到双端列表中。
3)哈希对象
列表对象的编码可以是ziplist或者hashtable。
ziplist 编码的哈希对象使用压缩列表作为底层实现,每当有新元素加入到哈希对象时,程序会将保存了 键的压缩列表节点推入到压缩列表表尾,然后再将保存了 值的压缩列表节点推入到压缩列表表尾。所以,键值紧挨在一起,先加入的键值在前,后加入的在后。举个例子:
hashtable编码的哈希对象使用字典作为底层实现,如下图:
编码转换:当哈希对象同时满足以下两个条件时才会使用ziplist,其他情况均会使用hashtable。
①哈希对象保存的所有键值对的键和值的字符串的长度都小于64字节;
②哈希对象保存的键值对数量小于512个;(以上两个值均可以通过配置文件修改相应参数)
对于使用ziplist编码的列表对象有以上两个条件不被满足时,编码转换就会发生,所有对象的编码就会变成hashtable编码,原先保存在压缩列表的元素也会转移到字典中。
4)集合对象集合对象的编码可以是intset或者hashtable。
intset 编码的集合对象使用整数集合作为底层实现,集合对象包含的所有对象都将保存在集合对象中。
hashtable编码的集合对象使用字典作为底层实现,字典的每一个键都是一个字符串对象,每个字符串对象包含了一个集合元素,而字典的值全部设为NULL。
编码转换:当集合对象同时满足以下两个条件时才会使用intset,其他情况均会使用hashtable。
①集合对象保存的元素都是整数值;(不可以通过配置文件修改相应参数)
②集合对象保存的元素数量不超过512个;(可以通过配置文件修改相应参数)
对于使用intset编码的列表对象有以上两个条件不被满足时,编码转换就会发生,所有对象的编码就会变成hashtable编码,原先保存在整数集合的元素也会转移到字典中。
5)有序集合对象
有序集合对象的编码可以是ziplist或者skiplist。
ziplist编码的压缩列表对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员(member),而第二个元素则保存元素的分值(score)。压缩列表内的集合元素按分值从小到大进行排序,分值较小的元素被放置在靠近表头的方向,而分值较大的元素则被放置在靠近表尾的方向。如下图所示:
skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表:
typedef struct zset {
zskiplist *zsl;
dict *dict;
} zset;
zset结构中的zsl跳跃表按分值从小到大保存了所有集合元素,每个跳跃表节点都保存了一个集合元素:跳跃表节点的object属性保存了元素的成员,而跳跃表节点的score属性则保存了元素的分值。除此之外,zset结构中的dict字典为有序集合创建了一个从成员到分值的映射,字典中的每个键值对都保存了一个集合元素:字典的键保存了元素的成员,而字典的值则保存了元素的分值。举个例子,如果前面price键创建的不是ziplist编码的有序集合对象,而是skiplist编码的有序集合对象,那么这个有序集合对象将会是图8-16所示的样子,而对象所使用的zset结构将会是图8-17所示的样子。
为什么有序集合需要同时使用跳跃表和字典来实现?
在理论上,有序集合可以单独使用字典或者跳跃表的其中一种数据结构来实现,但无论单独使用字典还是跳跃表,在性能上对比起同时使用字典和跳跃表都会有所降低。举个例子,如果我们只使用字典来实现有序集合,那么虽然以O(1)复杂度查找成员的分值这一特性会被保留,但是,因为字典以无序的方式来保存集合元素,所以每次在执行范围型操作——比如ZRANK、ZRANGE等命令时,程序都需要对字典保存的所有元素进行排序,完成这种排序需要至少O(NlogN)时间复杂度,以及额外的O(N)内存空间(因为要创建一个数组来保存排序后的元素)。另一方面,如果我们只使用跳跃表来实现有序集合,那么跳跃表执行范围型操作的所有优点都会被保留,但因为没有了字典,所以根据成员查找分值这一操作的复杂度将从O(1)上升为O(logN)。因为以上原因,为了让有序集合的查找和范围型操作都尽可能快地执行,Redis选择了同时使用字典和跳跃表两种数据结构来实现有序集合。
编码转换:当有序集合对象可以同时满足以下两个条件时,对象使用ziplist编码,其他情况均会使用skiplist。
①有序集合保存的元素数量小于128个;
②有序集合保存的所有元素成员的长度都小于64字节;(以上两个值可以通过配置文件修改相应参数)
对于使用ziplist编码的有序集合对象来说,当使用ziplist编码所需的两个条件中的任意一个不能被满足时,就会执行对象的编码转换操作,原本保存在压缩列表里的所有集合元素都会被转移并保存到zset结构里面,对象的编码也会从ziplist变为skiplist。
5)内存回收
redis在对象系统中构建了一个引用计数技术实现了内存回收机制。通过这一机制,程序可以通过跟踪对象的计数信息,在适当的时候自动释放对象并进行内存回收。
对象的引用计数信息会随着对象的使用状态而不断变化:
在创建一个新对象的时候,引用计数的值会被初始化为1
当对象被一个新程序使用的时候,它的引用计数值会被+1
当对象不再被一个程序使用时,它的引用计数值会被-1
当对象的引用计数值变为0时,对象所占用的内存会被释放。
6)对象共享
redis引用计数还用在了对象共享,多个键共享一个值对象需要两步,①将数据库键的值指针指向一个现有的值对象;②将被共享的值对象的引用计数增一。共享对象机制对于节约内存非常有帮助,数据库中保存的相同值对象越多,对象共享机制就能节约越多的内存。
目前,redis在初始化服务器会自动创建一万个字符串对象(0-9999的整数值),所以redis会共享0-9999的字符串对象。
问:Redis为什么只对包含整数值得字符串对象进行共享?
当服务器考虑将一个共享对象设置为键的值对象时,程序需要先检查给定的共享对象和键想创建的目标对象是否完全相同。一个共享对象保存的值越复杂,验证共享对象和目标对象是否相同所需的复杂度就会越高,消耗的CPU时间也会越多。故尽管共享更复杂的对象可以节约更多的内存,但受到CPU时间的限制,Redis只对包含整数值得字符串对象进行共享。