《Redis设计与实现》对象
本文最后更新于 2025年8月29日 下午
这一篇文章中的内容假定你已经读过且了解《Redis设计与实现》七个底层数据结构文章中的内容,如没有或者不了解Redis的七个底层数据结构,最好是先去那篇文章看看后再来看这篇。
对象
前面提到的7个数据结构,可以被视为在Redis系统中的几个基础数据结构。Redis则是基于这些数据结构来创建了一个对象系统。在这个对象系统中,有我们喜闻乐见的五种类型:
- 字符串对象:
SET msg "hello world" - 列表对象:
RPUSH numbers 1 3 5 - 哈希对象:
HMSET profile name Tom age 25 career Programmer - 集合对象:
SADD fruits apple banana cherry - 有序集合对象:
ZADD price 8.5 apple 5.0 6.0 cherry
| 对象 | 对象 type 属性的值 |
TYPE 命令的输出 |
|---|---|---|
| 字符串对象 | REDIS_STRING |
"string" |
| 列表对象 | REDIS_LIST |
"list" |
| 哈希对象 | REDIS_HASH |
"hash" |
| 集合对象 | REDIS_SET |
"set" |
| 有序集合对象 | REDIS_ZSET |
"zset" |
基本结构
1 | |
注意:
ptr指针指向的底层数据结构类型取决于encoding属性encoding属性记录了对象所使用的编码,该编码在Redis的底层c代码中被预定义为常量,为以下的其中一个
| 编码常量 | 编码所对应的底层数据结构 |
|---|---|
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中,每种
type都至少有两种不同的编码encoding
| 类型 | 编码 | 对象 |
|---|---|---|
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 |
使用跳跃表和字典实现的有序集合对象。 |
可以发现,有很多类型中都可能会用到REDIS_ENCODING_ZIPLIST作为底层数据结构,这取决于以下两点:
- 集合(列表,哈希,集合)中每一个元素的长度是否小于设定好的阈值
- 集合(列表,哈希,集合)的数量是否小于设定好的阈值
当且仅当以上两点都满足时,才会使用压缩列表作为底层数据结构。这里的考量是因为Redis是基于内存设计的数据库,对内存空间的利用极为敏感。
对于每一个可能会用到压缩列表作为底层数据结构的对象类型,Redis都可以自定义配置阈值:
1 | |
对于集合对象,Redis也可以配置阈值:
1 | |
- 使用
OBJECT ENCODING ...可以查看一个数据库建的值底层数据结构对象编码。概览如下:
| 对象所使用的底层数据结构 | 编码常量 | OBJECT ENCODING 命令输出 |
|---|---|---|
| 整数 | REDIS_ENCODING_INT |
"int" |
embstr 编码的简单动态字符串(SDS) |
REDIS_ENCODING_EMBSTR |
"embstr" |
| 简单动态字符串 | REDIS_ENCODING_RAW |
"raw" |
| 字典 | REDIS_ENCODING_HT |
"hashtable" |
| 双端链表 | REDIS_ENCODING_LINKEDLIST |
"linkedlist" |
| 压缩列表 | REDIS_ENCODING_ZIPLIST |
"ziplist" |
| 整数集合 | REDIS_ENCODING_INTSET |
"intset" |
| 跳跃表和字典 | REDIS_ENCODING_SKIPLIST |
"skiplist" |
接下来会介绍五种不同对象类型的具体内容
字符串对象 string
基本创建方式:SET [key] [value]
编码
int:如果输入的[value]为整数值,且可以用long类型标识,则该对象中,ptr指向的是一个long类型,且字符串编码encoding设置为int- 如果输入的
[value]长度太大导致没办法用long表示,则ptr会指向一个embstr或raw结构。
- 如果输入的
raw:如果输入的[value]为一个长度大于39字节的字符串,则该对象中,ptr指向的是一个sdshdr(简单动态字符串)结构。embstr:如果输入的[value]为一个长度小于等于39字节的字符串,则该对象中,ptr指向的也是一个sdshdr,并将字符串编码encoding设为embstr- 与
raw所使用的底层结构虽然相同(都是用的sdshdr),但是embstr在创建对象时,会创建一个连续的空间,将redisObject对象和sdshdr结构先后放在一起。因此,embstr的创建只需要分配一次内存,释放也只需要释放一次内存。
- 与
- 如果输入的
[value]为浮点数(可被long double类型标识),则该对象中,ptr指向的将是一个embstr或raw(取决于该浮点数转化为字符串之后的长度)- 在读取这类对象的时候,程序会将字符串对象的字符串值转为浮点数值,经过某些操作后,再将其转回为字符串值并存入对象中。
编码转换
在某些情况下,字符串对象的编码会发生转换:
- 原对象中存储的是
int对象,但是对其操作时加入了一个字符串值,则会将该对象的编码变为raw- 这种情况即使在对象修改后长度小于等于
39时也会发生。这是因为Redis对于每一个字符串类型的设计理念有着明确的分工。其中int是一个只读的整型embstr是一个只读的短字符串
- 如果要将
int修改为embstr,则会涉及到一些复杂的内存空间创建逻辑,且在代码层面上会使逻辑出现更多分支。 - 对象修改之后也许会再次遭到修改,那么用
raw这种支持更新的编码类型将更好。
- 这种情况即使在对象修改后长度小于等于
- 原对象中存储的是
embstr对象,但是对其操作时加入了一个另一个值,则会将该对象编码变为raw- 原因在于,Redis没有为
embstr编码的字符串编写任何修改程序,所以可以视为embstr对象是“只读”对象。因此,在需要对这类对象进行修改时,程序会将embstr修改为raw,然后进行修改。
- 原因在于,Redis没有为
列表对象 list
基本创建方式:RPUSH [key] [value1] [value2] ...
编码
ziplist:如果创建时给定的[value1] ...使用的是ziplist编码(需满足ziplist基本要求和满足列表使用ziplist的阈值),则该对象中,ptr指向的是一个ziplistlinkedlist:如果创建时个定的[value1] ...使用的是linkedlist编码(未满足ziplist基本要求和超出了列表使用ziplist的阈值),则该对象中,ptr指向的是一个linkedlist- 需要注意的是,在
linkedlist中,列表对象底层包含的是多个字符串sdshdr对象。
- 需要注意的是,在
编码转换
之前提过Redis底层会自动基于用户输入的value值决定是否使用ziplist,这也是基于之前提过几个阈值:
- 列表对象保存的所有字符串元素长度都小于阈值(默认为
64)字节; - 列表对象保存的元素数量小于阈值(默认为
512个);
所对应的在redis.conf配置文件中的配置条目为:
1 | |
若是对象原本使用的是ziplist编码,在对对象进行操作导致其无法满足以上两个条件的任意一个时,对象的编码转换操作就会执行:将原本在ziplist中的元素转移到linkedlist中,并将编码转为linkedlist。
- 注意: 这一过程是不可逆的。这意味着一旦编码变为了
linkedlist,便不会再变回ziplist。这是因为Redis考虑了防抖动机制和避免过多的内存空间重分配的情况。
哈希对象 hash
基本创建方式:HSET [key] [hash key1] [hash value1] ...
编码
ziplist:当对象保存的所有键值长度都小于配置阈值时,该对象的ptr指向的是一个ziplist,编码encoding将设为ziplistziplist内部结构为紧凑的键-值对。在添加一个新键值对时,哈希对象会先将键的压缩列表节点推到压缩列表尾部,然后再将值的压缩列表节点推到压缩列表尾部。
hashtable:当对象保存的键值中有任意一个不满足ziplist阈值,该对象的ptr将指向一个dict(字典结构),编码encoding将设为hashtable- 字典中的每一个键和值都是字符串对象
StringObject,底层实现都是sdshdr
编码转换
哈希对象同样基于两个阈值来决定是否使用ziplist编码:
- 哈希对象保存的所有键值对的键和值的字符串长度都小于阈值(默认
64字节); - 哈希对象保存的键值对数量小于
512个;
所对应的redis.conf配置文件中的配置条目为:
1 | |
哈希对象通过某些操作导致无法满足这两个条件时,该哈希对象的编码将变为hashtable:原本保存在压缩列表里的所有键值对都将转移并保存到字典中
- 注意: 与列表一样,此过程是不可逆的。不存在变成
hashtable后又变回ziplist的情况。
集合对象 set
基本创建方式:SADD [key] [value1] [value2] ...
编码
intset:输入的[value]值满足集合对象使用intset的阈值时,该对象会采用intset作为编码,ptr将指向一个intset结构hashtable:输入的[value]值没有满足集合对象使用的阈值时,该对象会采用hashtable作为编码,ptr将指向一个hashtable结构
编码转换
集合对象需要在同时满足以下两个条件时,使用intset编码:
- 集合对象保存的所有元素都是整数值;
- 集合对象保存的元素数量不超过设定阈值(默认
512个);
所对应的redis.conf配置文件中的配置条目为:
1 | |
对于intset编码的集合对象来说,当任意一个条件未被满足时,对象的编码转换操作就会被执行:原本整数集合中的所有元素都将被转移保存到字典中,并且对象的编码变为hashtable。
有序集合对象 zset
基本创建方式:ZADD [key] [score1] [item1] ...
有序集合对象中的元素按照给定的[score]从小到大排序
编码
-
ziplist:当对象元素都满足有序集合对象的配置阈值时,该对象将会使用ziplist进行编码,并且ptr将指向一个ziplist结构- 存储套路与哈希对象
hash相似,不过变成了前一个节点保存元素成员[item],后一个元素保存元素分支[score]。
- 存储套路与哈希对象
-
skiplist:当对象元素没有满足有序集合对象的配置阈值时,该对象将会使用skiplist进行编码,并且ptr将指向一个zset结构-
注意:
ptr并没有直接指向一个skiplist结构,而是一个zset结构。该结构的定义如下: -
typedef struct zset{ zskiplist *zsl; dict *dict; } -
可见该结构中,同时包含了一个字典和一个跳表
- 跳表: 按照元素的分值从小到大保存了所有集合元素。跳表节点的
object属性保存了元素成员[item],而跳表节点的score属性保存了元素的分值[score]。因此,可以通过使用这个跳表来执行范围型操作。 - 字典: 维护着集合元素中所有成员与其分值的映射。键为成员,值为对应成员的分值。这样可以以的复杂度快速找到对应成员的分值。
- 跳表: 按照元素的分值从小到大保存了所有集合元素。跳表节点的
-
并用两个结构的动机是:
- 充分利用跳表的特性来高效执行范围操作
- 充分利用字典的特性来高效执行查找操作
- 用两个结构来存储相同内容是否会导致数据重复而影响内存?不会,因为他们共享了元素的成员和分值。
-
编码转换
当有序集合对象可以同时满足一下两个条件时,对象是用ziplist编码:
- 有序集合保存的元素数量小于阈值(默认为
128个); - 有序集合保存的所有元素成员长度均小于阈值(默认
64字节);
所对应在redis.conf配置文件中的配置条目为:
1 | |
对于使用 ziplist 编码的有序集合对象来说, 当使用 ziplist 编码所需的两个条件中的任意一个不能被满足时, 程序就会执行编码转换操作, 将原本储存在压缩列表里面的所有集合元素转移到 zset 结构里面, 并将对象的编码从 ziplist 改为 skiplist 。
对象方法的多态性
在使用Redis命令的时候,可以发现有些领命在许多中不同的编码方式,或者说类型中都可以使用。这通常被视为是一种多态性。
Redis在执行一个命令的时候,会检查输入键的类型是否正确,然后决定是否执行给定的命令。如果不对,通常会返回一个类型错误。
我们可以注意到,redisObject结构中存在一个type属性。而Redis检查一个命令是否可以用于一个对象,便是取决于检查给定对象中这个type。
另一方面,我们又可以注意到,在Redis检查通过了一个命令可以用于一个对象后,也许对象的encoding并不唯一。这便凸显出了Redis的对象方法的多态性。打个比方,在type = list的情况下,LLEN指令可以用于获取列表对象的长度:
- 如果列表对象的编码为
ziplist, 那么说明列表对象的实现为压缩列表, 程序将使用ziplistLen函数来返回列表的长度; - 如果列表对象的编码为
linkedlist, 那么说明列表对象的实现为双端链表, 程序将使用listLength函数来返回双端链表的长度;
还有许多其他的命令也具有这样的多态性,如DEL, EXPIRE, TYPE等。而他们与LLEN这类指令的区别是,前者是基于type属性的多态,后者是基于encoding的多态。
内存回收
Redis在自己的对象系统中构建了一个引用计数(reference counting)技术实现只能得内存回收机制。目前看来,这个机制很像python中的计数器。
1 | |
机制与python很像:
- 创建新对象时,引用计数值初始化为
1; - 对象被新程序使用时,将调用
incrRefCount函数,使引用计数值增加1; - 对象不在被一个程序使用时,将调用
decrRefCount函数,是引用计数值减少1; - 对象的引用计数为
0时,对象所占用的内存将被释放
还有一个函数为resetRefCount,用于将对象的引用计数设置为0,但这一函数并不会释放对象内存,因为其通常在需要重新设置对象引用计数值时使用。
对象共享
Redis中使用引用计数器实现了对象共享的功能:假如之前创建了一个对象A且没有被内存回收,现在又要创建另一个对象B,且里面的元素与对象A的相同。这时候并不会真的为对象B的元素去创建一个新对象,而是将其直接与对象A的元素共享,并将其引用计数加一。
不过有如下内容需要 注意:
- 共享对象的值只能是整数类型
- 数据结构中嵌套了字符串对象的对象,也可以使用这些共享对象(也只能是整数类型)
共享对象的值只能是整数类型的解释为:
- 验证整数值的字符串对象所需的复杂度为
- 验证字符串值的字符串对象所需的复杂度为
- 验证多个值(对象)的对象,比如列表或哈希,那么复杂度为
- 出现相同的字符串值和包含了多个值的对象的概率,比出现相同整数值的概率要小的多
综上所述,并没有必要去给字符串值等字符串对象设计对象共享机制,这样自己很容易让程序的复杂度变高许多。
对象的空转时长
1 | |
redisObject结构还包含一个lru属性,该属性用于记录该对象最后一次被命令程序访问的时间。用这个时间,可以得到该对象的空转时间。在进行自动内存回收算法时将会用到该属性。
注意: 使用OBJECT IDLETIME [key]可以查看[key]对象的空转时长。但是这个命令并不会更新[key]对象的lru属性。