前面我们学习了redis各种数据结构,包括简单动态字符串、链表、字典、哈希表、整数集合、压缩列表,其实redis实际不是直接使用这些数据结构的,而是使用称为redis对象的数据结构:redisObject。
redis对象的定义如下:
typedef struct redisObject {
//对象类型
unsigned type:4;
//对象编码
unsigned encoding:4;
//最后一次被访问的时间
unsigned lru:LRU_BITS; /* lru time (relative to server.lruclock) */
//对象的引用计数
int refcount;
//底层数据结构
void *ptr;
} robj;
其中type表示的是具体数据类型,类型包括如下几种:
类型常量 | 对象的名称 |
---|---|
REDIS_STRING | 字符串对象 |
REDIS_LIST | 列表对象 |
REDIS_HASH | 哈希对象 |
REDIS_SET | 集合对象 |
REDIS_ZSET | 有序集合对象 |
redis中的键总是一个字符串对象,值可以是上述几种对象的一种。
当我们对redis键使用type命令时,返回的结果是值的类型,看下例:
127.0.0.1:6379> set msg "hello world"
OK
127.0.0.1:6379> type msg
string
127.0.0.1:6379> hset jack age 23
(integer) 1
127.0.0.1:6379> type jack
hash
键msg对应的值对象是字符串对象、键jack对应的值对象是哈希对象,下表展示了不同对象类型type命令的输出:
对象 | 对象type属性的值 | type命令的输出 |
---|---|---|
字符串对象 | REDIS_STRING | “string” |
列表对象 | REDIS_LIST | “list” |
哈希对象 | REDIS_HASH | “hash” |
集合对象 | REDIS_SET | ”set” |
有序集合对象 | REDIS_ZSET | “zset” |
encoding记录了对象的编码,也就是这个对象使用了什么样的底层数据结构实现的,下表展示了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 | 跳表和字典 |
下表展示了每种类型的对象使用的所有可能编码:
类型 | 编码 | 对象 |
---|---|---|
REDIS_STRING | REDIS_ENCODING_INT | 使用整数实现的字符串对象 |
REDIS_STRING | REDIS_ENCODING_EMBSTR | 使用embstr编码的简单动态字符串实现的字符串对象 |
REDIS_STRING | REDIS_ENCODING_RAW | 使用简单动态字符串实现的字符串对象 |
REDIS_LIST | REDIS_ENCODING_ZIPLIST | 使用压缩列表实现的列表对象 |
REDIS_LIST | REDIS_ENCODING_LINKEDLIST | 使用双端链表实现的列表对象 |
REDIS_HASH | REDIS_ENCODING_ZIPLIST | 使用压缩列表实现的哈希对象 |
REDIS_HASH | REDIS_ENCODING_HT | 使用字典实现的哈希对象 |
REDIS_SET | REDIS_ENCODING_INTSET | 使用整数集合实现的集合对象 |
REDIS_SET | REDIS_ENCODING_HT | 使用字典实现的集合对象 |
REDIS_ZSET | REDIS_ENCODING_ZIPLIST | 使用压缩列表实现的有序集合对象 |
REDIS_ZSET | REDIS_ENCODING_SKIPLIST | 使用跳表和字典实现的有序集合对象 |
使用object encoding命令可以查看一个键的值对象的编码,例如:
127.0.0.1:6379> set msg "hello redis"
OK
127.0.0.1:6379> type msg
string
127.0.0.1:6379> object encoding msg
"embstr"
127.0.0.1:6379> lpush languages chinese japanese english
(integer) 3
127.0.0.1:6379> type languages
list
127.0.0.1:6379> object encoding languages
"ziplist"
其中列表languages的编码是ziplist。
redis每种类型的对象都关联2到3种编码方式,不同的场景使用不同的编码方式,提高了内存使用率。后面会详细介绍每种类型的对象底层不同的编码方式,以及同一种类型的对象不同编码方式的转换和转换条件。
redis通过对象的引用计数字段(refcount)来实现内存的回收。随着对象的使用,该字段也会动态变化
除了实现内存回收外,还可以通过引用计数实现对象的共享,例如键A已经创建了包含整数值100的字符串对象,此时若键B也需要整数值100的字符串对象,那么完全可以将键B的值指针指向键A创建的整数值100的字符串对象,并将该对象的引用计数加1 。这种方式极大的提高了内存使用率。
在redis初始化时,会创建10000个从0到9999的字符串对象,这些对象就成为了共享对象。看下例:
127.0.0.1:6379> set k1 123
OK
127.0.0.1:6379> object refcount k1
(integer) 2
127.0.0.1:6379> set k2 123
OK
127.0.0.1:6379> object refcount k2
(integer) 3
首先设置k1的值为123,通过object refcount命令看到它的引用计数变为2了。这是因为当redis初始化创建该对象时就将该对象的refcount设置为1,这里又将键k1设置为123,该对象的refcount增加1,变为2。
接着又将k2设置为123,该对象的引用计数加1,变为3。
另外,redis只共享编码为整数的字符串对象。因为只有在共享对象和想要创建的对象相同时,才能使用共享对象。判断两个对象是否相等,对于编码为整数的字符串对象来说,时间复杂度为O(1);对于编码为字符串的字符串对象来说,时间复杂度为O(N);对于包含多个值的对象来说,时间复杂度为O(N^2^),所以为了提高效率,redis只共享编码为整数的字符串对象。
redis对象的lru属性记录了该对象最后被程序访问的时间,该属性可以用来计算键的空转时长,空转时长就是通过将当前的时间减去对象的lru时间计算得到的。例如,通过object idle命令可以查看键的空转时长是多少。
127.0.0.1:6379> object idletime k1
(integer) 700902
注意:命令object idletime比较特殊,它不会去修改对象的lru值。
另外,在内存回收的时候,空转时间较长的键可能被优先回收。
redis对象的ptr字段指向了底层实现,这个底层实现是由encoding值决定的。
字符串对象的类型(type)是string,编码(encoding)是int、raw、embstr的一种。
当字符串的值可以用long型整数来表示的时候,redis会将该字符串用int编码。redis会将整数值保存在对象结构的ptr属性里面。此时ptr的类型由原来的void*变为long*。例如,将k1设置为123时,object encoding返回int。
127.0.0.1:6379> set k1 123
OK
127.0.0.1:6379> object encoding k1
"int"
该字符串对象如图1示:
图1
当字符串对象保存的是一个字符串值,并且这个字符串值长度大于39字节,redis用简单动态字符串来保存这个值,并将对象的编码设置为raw。看下例子:
图2
当字符串对象保存的是一个字符串值,并且这个字符串值长度小于等于39字节,redis用embstr编码的方式来保存这个字符串值。
embstr编码是专门用来保存短字符串的一种优化编码方式。embstr编码和raw编码一样,都使用redisObject和sdshdr两种数据结构来表示字符串对象。但是raw编码会调用两次内存分配函数分别为redisObject和sdshdr分配内存空间,而embstr编码只会调用一次内存分配函数为redisObject和sdshdr分配一块连续的内存空间。如下图示,值为”hello”的字符串对象:
图3
这种方式的好处有:
最后,double类型也可以通过字符串对象来保存,看下例子:
127.0.0.1:6379> set k3 1.234
OK
127.0.0.1:6379> object encoding k3
"embstr"
当在需要的时候,redis会在double和字符串之间正确转换。
int编码的和embstr编码的字符串对象在满足一定的条件下会转换为raw编码来存储。
列表对象的编码可以是ziplist和linkedlist。
下图是一个ziplist编码的列表对象,其中节点元素是字节数组”hello”、整数值23、整数值35。
图4
linkedlist编码的列表对象底层使用双端链表实现,每个双端链表节点都是一个字符串对象,下图是一个linkedlist编码的列表对象示意图,其中链表包括两个字符串对象(这里的字符串对象简化了)。
图5
当列表对象同时满足如下两个条件时使用ziplist编码,否则使用linkedlist编码
当然,这两个值是可以在配置文件中修改的。
哈希对象的编码可以是ziplist和hashtable
ziplist编码的哈希对象,当有新的键值对需要插入到哈希对象时,首先会将保存键的压缩列表节点保存到压缩列表的表尾,再将保存值的压缩列表节点保存到压缩列表的表尾。因此同一个键值对总是会紧挨在一起,前一个是键,后一个是值。
下图所示压缩列表编码的哈希对象:
图6
该哈希对象包括了两个键值对age:23和name:jack,其中age:23是先添加的,name:jack是后添加的。
hashtable编码的哈希对象使用字典作为底层实现。
当哈希对象同时满足以下两个条件时,使用ziplist编码,否则使用hashtable编码
同样地,这两个数值可以通过配置文件进行配置。
集合的编码是intset和hashtable。intset编码的集合使用整数集合作为底层实现。
下面例子是一个包含三个整数的集合对象,使用了整数集合编码。
图7
hashtable编码的集合对象底层是用字典实现的,其中键就是集合的元素(字符串对象),值都是NULL。
当集合对象同时满足以下两个条件时,对象使用intset编码,否则使用hashtable编码
其中512这个值是可以通过配制文件修改的。
有序集合对象的编码可以是ziplist和skiplist。
ziplist编码的有序集合对象,底层使用压缩列表实现,每个集合元素由两部分组成,第一个保存元素的成员,第二个保存元素的分值。并且压缩列表集合内的元素按照分值从小到大排列。
例如,下图是一个压缩列表编码的有序集合对象的例子,其中元素都是按照分值从小到大排序的。
图8
skiplist实现的有序集合对象底层是通过跳表和字典实现的。底层数据结构定义如下:
typedef struct zset {
zskiplist *zsl;
dict *dict;
} zset;
其中跳表zsl按分值大小保存了有序集合的所有元素。
字典dict存储了成员到分值的映射。
有序集合对象中的每个成员都是一个字符串对象,每个分值都是一个double类型的浮点数。zsl和dict会共用字符串和分值对象,不会产生额外的内存。
当有序集合对象同时满足以下两个条件时,使用ziplist编码,否则使用skiplist进行编码
同样地,这两个值也是可以通过配制文件配制的。
redis有很多命令,有些命令是通用的,可以对任何键执行。而有些命令只能对特定的键执行。
在执行一个命令前,会首先根据键去查找值对象,然后确定值对象redisObject的type类型是否是执行命令所需要的类型,如果是的话就执行命令,否则返回错误。
除了进行类型检查,redis还可以根据redisObject的编码方式决定如何执行命令。
例如对于ziplist和linkedlist编码的列表对象,当执行命令llen时,显然会执行不同的方法获取列表的长度。
参考: