一、redis对象
1.1,介绍
上文介绍了redis的数据结构,从底层源码入手,图示了redis底层用到的数据结构。实际上redis并没有直接用到上文提到的SDS、双端链表、字典、压缩链表等,而是基于这些数据结构创建了一个对象系统,这个系统包含了字符串对象、列表对象、哈希对象、集合对象和有序集合对象五种基本类型的对象,每种对象都用到了至少一种我们上文提到的数据结构。redis中的这个对象,可以简单理解为我们编程语言中的对象,不过它更像是一种组合、封装和适配。本文将详细介绍redis的对象系统,并分析五种对象类型的底层实现和特性。
二、对象的类型与编码
redis使用对象来表示数据库中的键和值,每次我们在数据库中创建键值对时,都会创建两个对象,即键对象和值对象。使用命令创建一个简单的键值对,set name zys数据库中就存在了key=name、value=zys这两个对象。键值对中的键就是包含字符串“name”的字符串对象,键值对中的值就是包含字符串“zys”的字符串对象。
redis对象的定义如下:
1 | typedef struct redisObject { |
2 | usigned type:4;//类型 |
3 | usigned encoding:4;//编码 |
4 | void *ptr;//指向底层实现数据结构的指针 |
5 | ... |
6 | } |
redisObject有三个关键属性:类型type、编码encoding、指向底层实现的指针ptr。下面我们就来介绍这三个属性各自的含义。
2.1,对象的类型
type字段记录了redisObject的类型,这个类型其实指的就是这个对象类型,到底是个什么对象:字符串对象、列表对象、哈希对象、集合对象和有序集合对象。对于redis保存的键值对中的键而言,始终是一个字符串对象;而键值对中的键是上述五种类型中的一种。redis中有一个命令type可以查询键对应的值的对象类型。如下:
1 | -- 字符串类型对象 |
2 | 127.0.0.1:6379> set name zys |
3 | OK |
4 | 127.0.0.1:6379> type name |
5 | string |
6 | -- 列表类型对象 |
7 | 127.0.0.1:6379> rpush nums 1 2 3 4 |
8 | (integer) 4 |
9 | 127.0.0.1:6379> type nums |
10 | list |
11 | -- 哈希类型对象 |
12 | 127.0.0.1:6379> HMSET p name zys age 18 |
13 | OK |
14 | 127.0.0.1:6379> type p |
15 | hash |
16 | -- 集合类型对象 |
17 | 127.0.0.1:6379> sadd names zys lww |
18 | (integer) 2 |
19 | 127.0.0.1:6379> type names |
20 | set |
21 | -- 有序集合类型对象 |
22 | 127.0.0.1:6379> zadd price 4 apple 3 banana |
23 | (integer) 2 |
24 | 127.0.0.1:6379> type price |
25 | zset |
下表为不同类型值对象映射相关信息
| 对象名称 | 类型常量 | type命令返回值 |
|---|---|---|
| 字符串对象 | REDIS_STRING | “string” |
| 列表对象 | REDIS_LIST | “list” |
| 哈希对象 | REDIS_HASH | “hash” |
| 集合对象 | REDIS_SET | “set” |
| 有序集合对象 | REDIS_ZSET | “zset” |
2.2,编码和底层实现
对象的ptr指向了对象底层实现的数据结构,而这些数据结构则由encoding决定。encoding指定了底层数据结构的类型,也就是说明了这个对象是由什么数据结构实现的。不同类型的对象底层编码对应如下表:
| 对象类型 | 编码 | 对象 | object输出 |
|---|---|---|---|
| REDIS_STRING | REDIS_ENCODING_INT | 使用整数值实现的字符串对象 | int |
| REDIS_STRING | REDIS_ENCODING_EMBSTR | 使用embstr编码简单动态字符串实现的字符串对象 | embstr |
| REDIS_STRING | REDIS_ENCODING_RAW | 使用简单动态字符串实现的字符串对象 | raw |
| REDIS_LIST | REDIS_ENCODING_ZIPLIST | 使用压缩列表实现的列表对象 | zippiest |
| REDIS_LIST | REDIS_ENCODING_LINKEDLIST | 使用双端链表实现的列表对象 | linked list |
| REDIS_HASH | REDIS_ENCODING_ZIPLIST | 使用压缩列表实现的哈希对象 | zippiest |
| REDIS_HASH | REDIS_ENCODING_HT | 使用字典实现的哈希对象 | hash table |
| REDIS_SET | REDIS_ENCODING_INTSET | 使用整数集合实现的集合对象 | inset |
| REDIS_SET | REDIS_ENCODING_HT | 使用字典实现的集合对象 | hash table |
| REDIS_ZSET | REDIS_ENCODING_ZIPLIST | 使用压缩列表实现的集合对象 | zippiest |
| REDIS_ZSET | REDIS_ENCODING_SKIPLIST | 使用跳跃表和字典实现的有序集合对象 | skiplist |
我们可以使用命令object encoding(查看对象的encoding属性)简单实验一下:
1 | -- embstr编码简单动态字符串 |
2 | 127.0.0.1:6379> set name zys |
3 | OK |
4 | 127.0.0.1:6379> object encoding name |
5 | "embstr" |
6 | -- 简单动态字符串 |
7 | 127.0.0.1:6379> set long_name "zhaoyansheng is very handsome.what do you think?" |
8 | OK |
9 | 127.0.0.1:6379> object encoding long_name |
10 | "raw" |
11 | -- 整数值 |
12 | 127.0.0.1:6379> set num 1 |
13 | OK |
14 | 127.0.0.1:6379> object encoding num |
15 | "int" |
16 | 127.0.0.1:6379> sadd nums 1 2 3 |
17 | (integer) 3 |
18 | 127.0.0.1:6379> object encoding nums |
19 | "intset" |
20 | 127.0.0.1:6379> object encoding nums |
21 | "hashtable" |
通过测试我们可以知道,通过encoding来指定对象实际使用的数据结构,而不是一个对象类型指定一种数据结构,而是根据特定的条件来进行匹配使用。接下来,我们分别介绍五种对象类型在不同情况下的编码情况。
2.2.1,字符串对象
字符串对象的编码可以是int、raw或者embstr。
如果一个字符串对象保存的是整数值,可以用long类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性里,此时对象的编码为int。

如果一个字符串对象保存的是一个字符串值,并且这个字符串的长度大于39字节,那么字符串对象将使用简单动态字符串SDS来保存这个字符串值,此时对象的编码为raw。

如果一个字符串对象保存的是一个字符串值,并且这个字符串的长度小于等于39字节,那么字符串对象将使用embstr来保存这个字符串值,此时对象的编码为embstr。embstr编码主要是针对短字符串的一种优化编码方式,和raw编码一样,都使用redisObject结构和sdshdr结构来表示字符串对象,但是raw会进行两次内存分配函数分别创建redisObject和sdshdr,而embstr编码则通过一次内存分配函数来分配一块连续的空间,空间中依次包含redisObject和sdshdr两个结构。

2.2.2,列表对象
列表对象编码是ziplist或者linkedlist。
ziplist使用压缩列表作为底层实现,每个压缩节点保存了一个列表元素。假设列表有三个元素分别为“1”、“test”、“9”,那么通过ziplist保存如下:

linkedlist编码底层使用双端链表,每个双端链表节点保存了一个字符串对象,每个字符串对象保存一个队列元素。上述列表通过linkedlist保存如下(注:使用StringObject代表字符串对象,具体实现细节同上2.2.1,字符串对象):

当列表对象同时满足以下两个条件时,列表对象使用ziplist编码:
- 列表对象保存的所有字符串元素的长度都小于64字节
- 列表对象保存的元素数量小于512个;不能满足这两个条件的列表对象会使用linkedlist编码。
2.2.3,哈希对象
哈希对象的编码可以是ziplist或者hashtable。
当使用ziplist作为底层实现时,键值对的键先加入到压缩列表表尾,然后键值对的值再加入到表尾。也就是键值对的键和值相邻;先添加哈希对象的键值对靠近表头,后添加进入哈希对象的键值对靠近表尾。使用ziplist作为底层实现的哈希对象图示如下:

当使用hashtable作为底层实现时,哈希对象的每一个键值对都作为字典的键值对保存。字典的键和值都是字符串对象,对象分别保存了键值对的键和值。使用hashtable作为底层实现的哈希对象图示如下:

当哈希对象可以同时满足以下两个条件时,哈希对象使用ziplist编码:
- 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节
- 哈希对象保存的键值对数量小于512个;不能满足这两个条件的哈希对象需要使用hashtable编码。
2.2.4,集合对象
集合对象的编码可以是intset或者hashtable。
当使用intset作为底层实现时,集合对象包含的所有元素都被保存在整数集合里面。图示如下:
1 | redis> sadd mums 1 2 3 |
2 | (integer) 3 |

intset-set-object
当使用hashtable作为底层实现时,字典的每个键都是一个字符串对象,每个字符串对象包含了一个集合元素,而字典的值则全部设置为NULL。图示如下:
1 | redis> sadd strs zhao yan sheng |
2 | (integer) 3 |

当集合对象可以同时满足以下两个条件时,对象使用intset编码:
- 集合对象保存的所有元素都是整数值
- 集合对象保存的元素数量不超过512个;不能满足时使用hashtable编码。
2.2.5,有序集合对象
有序集合对象的编码可以是ziplist或者skiplist。
当使用ziplist编码作为底层实现时,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员(member),第二个元素保存元素的分值(score)。压缩列表内集合元素按照分值从小到大排序,分值较小的元素放置在表头,分值较大的放置在表尾。图示和上述哈希对象ziplist表示类似,这里就不再给出。
zset结构持有了跳跃表zsl和字典dict两个数据结构来实现有序集合对象。每个跳跃表节点保存了一个集合元素,跳跃表节点的object属性保存了元素的成员,而跳跃表节点的score属性则保存了元素的分值。通过这个跳跃表,可以对集合中的元素进行范围查询、排序等功能。dict字典结构则记录了有序集合中元素成员到分值的一个映射,通过这个字典结构,可以快速获取每个成员的分值。下面举例图示说明这个结构:

当有序集合对象可以同时满足以下两个条件时,使用ziplist进行编码:
- 有序集合保存的元素数量小于128个
- 有序集合保存的所有元素成员的长度都小于64字节;不满足上述情况的有序集合对象使用skiplist编码。