redis对象

redis对象

前面我们学习了redis各种数据结构,包括简单动态字符串、链表、字典、哈希表、整数集合、压缩列表,其实redis实际不是直接使用这些数据结构的,而是使用称为redis对象的数据结构:redisObject。

1. redis对象定义

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;

1.1 对象类型-type

其中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”

1.2 对象编码-encoding

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种编码方式,不同的场景使用不同的编码方式,提高了内存使用率。后面会详细介绍每种类型的对象底层不同的编码方式,以及同一种类型的对象不同编码方式的转换和转换条件。

1.3 对象引用计数-refcount

redis通过对象的引用计数字段(refcount)来实现内存的回收。随着对象的使用,该字段也会动态变化

  • 创建一个新的对象时,refcount会被初始化为1。
  • 当对象被新的程序引用时,refcount计数递增1。
  • 当对象不再被一个程序使用时,refcount计数递减1。
  • 当refcount等于0的时候,对象所占用的内存会被释放。

除了实现内存回收外,还可以通过引用计数实现对象的共享,例如键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只共享编码为整数的字符串对象。

1.4 对象的空转时长-lru

redis对象的lru属性记录了该对象最后被程序访问的时间,该属性可以用来计算键的空转时长,空转时长就是通过将当前的时间减去对象的lru时间计算得到的。例如,通过object idle命令可以查看键的空转时长是多少。

127.0.0.1:6379> object idletime k1
(integer) 700902

注意:命令object idletime比较特殊,它不会去修改对象的lru值。

另外,在内存回收的时候,空转时间较长的键可能被优先回收。

1.5 对象的底层实现-ptr

redis对象的ptr字段指向了底层实现,这个底层实现是由encoding值决定的。

2. 字符串对象

字符串对象的类型(type)是string,编码(encoding)是int、raw、embstr的一种。

2.1 int编码的字符串对象

当字符串的值可以用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示:

redis对象_第1张图片

图1

2.2 raw编码的字符串对象

当字符串对象保存的是一个字符串值,并且这个字符串值长度大于39字节,redis用简单动态字符串来保存这个值,并将对象的编码设置为raw。看下例子:

redis对象_第2张图片

图2

2.3 embstr编码的字符串对象

当字符串对象保存的是一个字符串值,并且这个字符串值长度小于等于39字节,redis用embstr编码的方式来保存这个字符串值。

embstr编码是专门用来保存短字符串的一种优化编码方式。embstr编码和raw编码一样,都使用redisObject和sdshdr两种数据结构来表示字符串对象。但是raw编码会调用两次内存分配函数分别为redisObject和sdshdr分配内存空间,而embstr编码只会调用一次内存分配函数为redisObject和sdshdr分配一块连续的内存空间。如下图示,值为”hello”的字符串对象:

这里写图片描述

图3

这种方式的好处有:

  • 分配内存次数降为1次
  • 释放内存次数降为1次
  • 内存具有连续性,更好利用缓存

最后,double类型也可以通过字符串对象来保存,看下例子:

127.0.0.1:6379> set k3 1.234
OK
127.0.0.1:6379> object encoding k3
"embstr"

当在需要的时候,redis会在double和字符串之间正确转换。

2.4 字符串对象各编码之间的转换

int编码的和embstr编码的字符串对象在满足一定的条件下会转换为raw编码来存储。

  • 向一个int编码的字符串对象追加字符时,会直接转换成raw编码
  • embstr编码的字符串对象执行任何修改时,会直接转换成raw编码,可以认为embstr编码的字符串对象是只读的

3 列表对象

列表对象的编码可以是ziplist和linkedlist。

3.1 ziplist编码的列表对象

下图是一个ziplist编码的列表对象,其中节点元素是字节数组”hello”、整数值23、整数值35。

redis对象_第3张图片

图4

3.2 linkedlist编码的列表对象

linkedlist编码的列表对象底层使用双端链表实现,每个双端链表节点都是一个字符串对象,下图是一个linkedlist编码的列表对象示意图,其中链表包括两个字符串对象(这里的字符串对象简化了)。

redis对象_第4张图片

图5

3.3 列表对象各编码之间的转换

当列表对象同时满足如下两个条件时使用ziplist编码,否则使用linkedlist编码

  • 列表对象保存的所有字符串对象元素的长度都小于64字节
  • 列表对象保存的元素数量小于512个

当然,这两个值是可以在配置文件中修改的。

4. 哈希对象

哈希对象的编码可以是ziplist和hashtable

4.1 ziplist编码的哈希对象

ziplist编码的哈希对象,当有新的键值对需要插入到哈希对象时,首先会将保存键的压缩列表节点保存到压缩列表的表尾,再将保存值的压缩列表节点保存到压缩列表的表尾。因此同一个键值对总是会紧挨在一起,前一个是键,后一个是值。

下图所示压缩列表编码的哈希对象:

redis对象_第5张图片

图6

该哈希对象包括了两个键值对age:23和name:jack,其中age:23是先添加的,name:jack是后添加的。

4.2 hashtable编码的哈希对象

hashtable编码的哈希对象使用字典作为底层实现。

4.3 哈希对象各编码之间的转换

当哈希对象同时满足以下两个条件时,使用ziplist编码,否则使用hashtable编码

  • 哈希对象保存的键值对的键和值长度都小于64字节
  • 哈希对象保存的键值对数量小于512个

同样地,这两个数值可以通过配置文件进行配置。

5. 集合对象

集合的编码是intset和hashtable。intset编码的集合使用整数集合作为底层实现。

5.1 intset编码的集合对象

下面例子是一个包含三个整数的集合对象,使用了整数集合编码。

redis对象_第6张图片

图7

5.2 hashtable编码的集合对象

hashtable编码的集合对象底层是用字典实现的,其中键就是集合的元素(字符串对象),值都是NULL。

5.3 集合对象各编码之间的转换

当集合对象同时满足以下两个条件时,对象使用intset编码,否则使用hashtable编码

  • 集合对象保存的值都是整数
  • 集合对象保存元素数量不超过512个

其中512这个值是可以通过配制文件修改的。

6. 有序集合对象

有序集合对象的编码可以是ziplist和skiplist。

6.1 ziplist编码的有序集合对象

ziplist编码的有序集合对象,底层使用压缩列表实现,每个集合元素由两部分组成,第一个保存元素的成员,第二个保存元素的分值。并且压缩列表集合内的元素按照分值从小到大排列。

例如,下图是一个压缩列表编码的有序集合对象的例子,其中元素都是按照分值从小到大排序的。

redis对象_第7张图片

图8

6.2 skiplist实现的有序集合对象

skiplist实现的有序集合对象底层是通过跳表和字典实现的。底层数据结构定义如下:

typedef struct zset {
  zskiplist *zsl;
  dict *dict;
} zset;

其中跳表zsl按分值大小保存了有序集合的所有元素。

字典dict存储了成员到分值的映射。

有序集合对象中的每个成员都是一个字符串对象,每个分值都是一个double类型的浮点数。zsl和dict会共用字符串和分值对象,不会产生额外的内存。

6.3 有序集合对象各编码之间换转换

当有序集合对象同时满足以下两个条件时,使用ziplist编码,否则使用skiplist进行编码

  • 有序集合保存的所有元素成员的长度都小于64字节
  • 有序集合保存的元素数量小于128个

同样地,这两个值也是可以通过配制文件配制的。

7. 类型检查和命令多态

redis有很多命令,有些命令是通用的,可以对任何键执行。而有些命令只能对特定的键执行。

7.1 类型检查

在执行一个命令前,会首先根据键去查找值对象,然后确定值对象redisObject的type类型是否是执行命令所需要的类型,如果是的话就执行命令,否则返回错误。

7.2 命令多态

除了进行类型检查,redis还可以根据redisObject的编码方式决定如何执行命令。

例如对于ziplist和linkedlist编码的列表对象,当执行命令llen时,显然会执行不同的方法获取列表的长度。

参考:

  1. Redis设计与实现. 黄健宏著

你可能感兴趣的:(redis,对象,数据结构,redis)