1、对象的类型与编码
Redis使用对象来表示数据库中的键和值,每次我们在Redis的数据库中新创建一个键值对,我们至少会创建两个对象,一个键对象,另一个值对象。
每个对象都由一个redisObject结构表示,如下:
1.1、对象的类型和编码
type属性记录了对象的类型:
类型常量 | 对象的名称 |
REDIS_STRING | 字符串对象 |
REIDS_LIST | 列表对象 |
REDIS_HASH | 哈希对象 |
REDIS_SET | 集合对象 |
REDIS_ZSET | 有序集合对象 |
encoding记录了对象使用的编码,也就是说这个对象使用了什么数据结构作为对象的底层实现
每个类型的对象都至少使用了两种不同的编码:
使用OBJECT ENCODING key(key就是你要查找的键)命令可以查看一个数据库键的值对象的编码 。
2、字符串对象
- 字符串对象的编码可以是int、raw、embstr
编码 | 条件 |
raw | 1、字符串值长度 > 32字节 |
embstr | 1、字符串值长度 <= 32字节 |
- raw和embstr的比较 :
- embstr编码创建字符串对象所需的内存分配次数从raw编码的两次降低为一次
- 释放embstr编码的字符串只需调用一个内存释放,raw两次
- embstr编码的字符串所有数据保存在一块连续的内存中,更好来的利用缓存带来的优势、
- 可以用long、double类型表示的浮点数在Redis中也是作为字符串值保存。程序会先将浮点数转换为字符串值,然后再保存转换所得的字符串值。在有需要的时候,程序会将保存的字符串值转回浮点数,执行操作。
2.1、编码的转换
- int ==> raw : 对int对象执行一些命令(比如APPEND),使得对象保存的不再是整数值,变成一个字符串值,那么字符串对象的编码从int变成raw
- embstr ==> raw : redis没有为embstr编码的字符串对象编写任何修改程序,当我们对embstr编码的字符串对象执行任何修改命令时,程序会先将embstr转换为raw,再执行修改命令。
3、列表对象
- 列表对象编码可以是ziplist、linkedlist
编码 | 条件 | 修改命令 |
ziplist | 1、列表对象保存的所有字符串元素的长度都小于64字节 | list-max-ziplist-value |
2、列表对象保存的元素数量小于512个 | list-max-ziplist-entries | |
linkedlist | 上面不满足任一条件 |
4、哈希对象
- 哈希对象的编码可以是ziplist、hashtable
编码 | 条件 | 修改命令 |
ziplist | 1、哈希对象保存的所有键值对的键和值的字符串长度都小于64字节 | hash-max-ziplist-value |
2、哈希对象保存的键值对数量小于512个 | hash-max-ziplist-entries | |
hashtable | 上面不满足任一条件 |
5、集合对象
- 集合对象的编码可以是intset、hashtable
编码 | 条件 | 修改命令 |
intset | 1、集合对象保存的所有元素都是整数值 | |
2、集合对象保存的元素数量不超过512个 | set-max-intset-entries | |
hashtable | 上面不满足任一条件 |
6、有序集合对象
- 有序集合的编码可以是ziplist或者skiplist
编码 | 条件 |
|
|
ziplist | 1、有序集合保存的元素数量小于128个 | zset-max-ziplist-entries | |
2、有序集合保存的所有元素成员的长度都小于64字节 | zset-max-ziplist-value | ||
skiplist | 上面不满足任一条件 |
- ziplist编码的压缩列表对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员(member),而第二个元素则保存元素的分值(score)。压缩列表内的集合元素按分值从小到大进行排序,分值较小的元素被放置在靠近表头的方向,而分值较大的元素则被放置在靠近表尾的方向。
- skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表:
-
- zset结构中的zsl跳跃表按分值从小到大保存了所有集合元素,每个跳跃表节点都保存了一个集合元素。通过这个跳跃表,程序可以对有序集合进行范围型操作,比如ZRANK、ZRANGE等命令就是基于跳跃表API来实现的
- zset结构中的dict字典为有序集合创建了一个从成员到分值的映射,字典中的每个键值对都保存了一个集合元素:字典的键保存了元素的成员,而字典的值则保存了元素的分值。通过这个字典,程序可以用O(1)复杂度查找给定成员的分值,ZSCORE命令就是根据这一特性实现的
- 为什么有序集合需要同时使用跳跃表和字典来实现?
- 在理论上,有序集合可以单独使用字典或者跳跃表的其中一种数据结构来实现,但无论单独使用字典还是跳跃表,在性能上对比起同时使用字典和跳跃表都会有所降低。举个例子,如果我们只使用字典来实现有序集合,那么虽然以O(1)复杂度查找成员的分值这一特性会被保留,但是,因为字典以无序的方式来保存集合元素,所以每次在执行范围型操作——比如ZRANK、ZRANGE等命令时,程序都需要对字典保存的所有元素进行排序,完成这种排序需要至少O(NlogN)时间复杂度,以及额外的O(N)内存空间(因为要创建一个数组来保存排序后的元素)。另一方面,如果我们只使用跳跃表来实现有序集合,那么跳跃表执行范围型操作的所有优点都会被保留,但因为没有了字典,所以根据成员查找分值这一操作的复杂度将从O(1)上升为O(logN)。因为以上原因,为了让有序集合的查找和范围型操作都尽可能快地执行,Redis选择了同时使用字典和跳跃表两种数据结构来实现有序集合。
7、内存回收
Redis在自己的对象系统中构建了一个引用计数(reference counting)技术实现的内存回收机制,通过这一机制,程序可以通过跟踪对象的引用计数信息,在适当的时候自动释放对象并进行内存回收。
每个对象的引用计数信息由redisObject结构的refcount属性记录:
对象的引用计数信息会随着对象的使用状态而不断变化:
- 在创建一个新对象时,引用计数的值会被初始化为1
- 当对象被一个新程序使用时,它的引用计数值会被增一
- 当对象不再被一个程序使用时,它的引用计数值会被减一
- 当对象的引用计数值变为0时,对象所占用的内存会被释放
8、对象共享
对象的引用计数属性还带有对象共享的作用,范围是0~9999,共享对象不单单只有字符串键可以使用,那些在数据结构中嵌套了字符串对象的对象(linkedlist编码的列表对象、hashtable编码的哈希对象、hashtable编码的集合对象,以及zset编码的有序集合对象)都可以使用这些共享对象
让多个键共享同一个值对象需要执行以下两个步骤:
1、将数据库键的值指针指向一个现有对象
2、将被共享的值对象的引用计数增加一
为什么Redis不共享包含字符串的对象?
当服务器考虑将一个共享对象设置为键的值对象时,程序需要先检查给定的共享对象和键想创建的目标对象是否完全相同,只有在共享对象和目标对象完全相同的情况下,程序才会将共享对象用作键的值对象,而一个共享对象保存的值越复杂,验证共享对象和目标对象是否相同所需的复杂度就会越高,消耗的CPU时间也会越多:□ 如果共享对象是保存整数值的字符串对象,那么验证操作的复杂度为O(1);□ 如果共享对象是保存字符串值的字符串对象,那么验证操作的复杂度为O(N);□ 如果共享对象是包含了多个值(或者对象的)对象,比如列表对象或者哈希对象,那么验证操作的复杂度将会是O(N 2)。因此,尽管共享更复杂的对象可以节约更多的内存,但受到CPU时间的限制,Redis只对包含整数值的字符串对象进行共享
9、 对象的空转时长
除了前面介绍过的type、encoding、ptr和refcount四个属性之外,redisObject结构包含的最后一个属性为lru属性,该属性记录了对象最后一次被命令程序访问的时间。
OBJECT IDLETIME命令可以打印出给定键的空转时长,这一空转时长就是通过将当前时间减去键的值对象的lru时间计算得出的。
如果服务器打开了maxmemory选项,并且服务器用于回收内存的算法为volatile-lru或者allkeys-lru,那么当服务器占用的内存数超过了maxmemory选项所设置的上限值时,空转时长较高的那部分键会优先被服务器释放,从而回收内存。