1 Redis 对象
Redis主要的数据结构为:
1. 简单动态字符串(SDS)
2. 双向链表
3. 字典
4. 压缩列表
5. 整数集合
6. 跳跃表
Redis并没有直接使用这些数据结构来存储数据,而是基于这些数据结构勾线一个对象系统,名为redisObject。这个系统包含:
1. 字符串对象
2. 列表对象
3. 哈希对象
4. 集合对象
5. 有序集合对象
这五种类型对象,每种对象通过使用前面所介绍的一种数据结构或者多种数据结构来组成。这样使用的好处是可以针对不同的使用场景,为对象设置多种不同的数据结构,从而优化使用效率。
Redis中的每个对象都是由一个redisObject结构表示:
type:记录了对象的类型,Redis有以下对象类型:
REDIS_STRING 字符串对象 string
REDIS_LIST 列表对象 list
REDIS_HASH 哈希对象 hash
REDIS_SET 集合对象 set
REDIS_ZSET 有序集合对象 zset
以上所说的对象都是对值来说,而键名,只有字符串对象一种类型。
ptr指针指向对象的底层实现数据结构;
encoding属性表示对象使用的编码:编码有以下几种:
REDIS_ENCODING_INT long类型整数
REDIS_ENCODING_EMBSTR embstr编码的简单动态字符串
REDIS_ENCODING_RAW 简单动态字符串
REDIS_ENCODING_HT 字典
REDIS_ENCODING_LINKEDLIST 双向链表
REDIS_ENCODING_ZIPLIST 压缩链表
REDIS_ENCODING_INTSET 整数集合
REDIS_ENCODING_SKIPLIST 跳跃表和字典
2 编码和底层实现
每一种类型的对象都至少使用了两种不同的编码,对应关系如下:
2.1 字符串对象
字符串的编码可以是int、raw和embstr。
如果字符串对象保存的是整数值,并且这个整数值可以用long类型来表示,则该字符串的编码为int
比如:set number 10086
如果字符串对象保存的是一个字符串值,并且该字符串长度大于32字节,则编码为raw(SDS)。
比如:set stroy “long long age there is lived a king …”
以上例子在内存中的模型示意图为:
如果字符串长度小于32字节,那么编码为embstr。
embstr编码是专门用来存储短字符串的优化编码,这种编码和raw编码一样都是SDS结构。但是区别为:
- raw编码会调用两次内存分配函数来创建redisObject和sdshdr,embstr只通过一次内存分配来分配一块连续的空间。
- embstr只需要释放一次内存,而raw需要释放两次。
- embstr的数据都保存在一块连续的内存中,而raw则不能保证这一点。
比如:set msg “hello”。
在编码转换时,因为redis对embstr没有修改程序,因此在修改embstr的编码时,必须先将embstr转换为raw。
将对int类型的数据增加字符串时,会将int会转换成raw。
比如:
set number 10086 -------int
append number “ is a good number” ------raw
2.2 列表对象
列表对象的编码可以是ziplist或者linkedlist。
ziplist
比如:RPUSH number 1 “aaa” 5
则number的编码会是ziplist:
linkedlist
linkedlist编码的列表对象使用双向链表作为底层实现,每个双向链表节点都保存了一个字符串对象,每个对象都保存了一个列表元素。如果之前的例子编码不是ziplist而是linkedlist,那结构就是如下所示:
在编码转换时:当列表对象同时满足以下两个条件,则使用ziplist编码:
4. 列表对象保存的所有字符串元素的长度都小于64字节。
5. 列表对象的元素个数少于512个。
以上两个条件的上限值可以通过修改配置文件修改。
list-max-ziplist-value和list-max-ziplist-entries
2.3 哈希对象
哈希对象的编码可以是ziplist或者hashtable。
ziplist编码的哈希对象使用压缩列表作为底层实现。
hashtable编码的哈希对象使用字典作为底层实现。
所对应的结构分别为:
编码转换的条件为,同时满足以下两个条件则使用ziplist编码:
6. 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节。
7. 哈希对象保存的键值对数量小于512个。
以上两个条件可以通过修改配置文件修改,配置项为:
hash-max-ziplist-value和hash-max-ziplist-entries。
2.4 集合对象
集合对象的编码可以是lntset或者hashtable。
inset编码的集合对象使用整数集合作为底层实现。
比如:SADD number 1 3 5
hashtable编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,每个字符串对象包含了一个集合元素,而值都指向NULL。
比如:
SAD Dfruits “apples” “banana” “cherry”
编码转换时,当集合同时满足以下两个条件时,则使用inset编码:
- 集合元素都为整数值。
- 集合对象保存的元素数量不超过512个。
两个上限值可以修改,配置项为:set-max-inset-entries。
2.5 有序集合对象
有序集合的编码可以是ziplist和skiplist
ziplist编码的压缩列表作为底层实现,每个集合元素使用两个紧靠一起的压缩列表节点来保存,第一个节点保存元素成员,第二个保存元素的分值。
压缩列表内的集合元素按照分值的大小进行排序。分值较小的元素被放置在靠近表头的位置,较大的被分值在表尾。
比如:
ZADD price 8.5 apple 5.0 banana 6.0 cherry
结构为:
zset结构使用跳跃表和字典来作为底层实现。
每个跳跃表的节点保存了一个集合元素:跳跃表节点的object属性保存了元素的成员,而跳跃表节点的score属性保存了分值。通过跳跃表可以对有序集合进行范围型操作。
字典结构为有序集合创建了从成员到分值的映射,字典中每个键值对都保存了一个集合元素。通过字典,程序可以通过O(1)的时间复杂度查找到给定成员的分值。
zset同时使用跳跃表和字典来保存有序集合元素,两种数据结构都会通过指针来共享元素的成员和分值,不会造成内存浪费,并且可以同时使用两种数据的API,提高了数据查询和插入的效率。
编码转换时,同时满足以下两个条件,则使用ziplist:
- 有序集合的元素数量小于128个;
- 有序集合保存的所有元素成员的长度小于64字节。
以上两个条件可以在配置文件中修改,配置项为:
zset-max-ziplist-entries和zset-max-ziplist-value
3 内存回收
因为C语言不具备内存回收功能,所以Redis在自己的对象系统中构建了引用计数器,来实现内存回收机制。在redisObject结构中,参数refcount表示引用数:
其规则如下:
- 创建一个对象时,计数器初始化为1
- 对象被一个新程序使用时,计数器+1
- 不再被使用时,计数器-1
- 当计数器的值为0时,对象会被回收。
对象的整个生命周期可以划分为创建对象、使用对象、释放对象。
4 对象共享
对象的引用计数器除了实现内存回收机制以外,还带有对象共享的作用。
假设键A创建了一个包含整数值100的字符串对象作为值的对象。而当键B也需要做同样的操作时,会将指针指向A创建的对象中,并且该被指向的对象(100整数值)引用计数加1。
这个特性在数据量非常大的时候,非常有用,将大大减少内存的浪费。
4.1 对象的空转时长。
在redisObject结构中,除了前面介绍过的type,encoding、ptr和refcount外,还有最后一个属性lru属性,记录了对象最后一次被访问的时间。
通过OBJECT IDLETIME命令可以打印出给定键的空转时长,这一时间是通过当前时间减去lru时间计算得出的。
该时间有何作用呢,如果redis服务器打开了maxmemory最大使用内存的设置时,当redis占用的内存超过该值是,空转时长较长的那部分对象就会优先被服务器释放,从而回收内存。