《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
2
3
4
5
6
7
8
9
10
typedef struct redisObject {
// 该对象的类型
unsigned type:4;
// 编码
unsigned encoding:4;
// 指向底层实现数据结构的指针
void *ptr;
// ...

} robj;

注意:

  • 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
2
3
4
5
6
7
8
9
10
11
12
# redis.conf 配置
# 列表对象
list-max-ziplist-value 64 # 单个元素大小阈值(字节)
list-max-ziplist-entries 512 # 元素数量阈值

# 哈希对象
hash-max-ziplist-entries 512 # 字段数量阈值
hash-max-ziplist-value 64 # 字段值长度阈值

# 有序集合对象
zset-max-ziplist-entries 128 # 元素数量阈值
zset-max-ziplist-value 64 # 元素值长度阈值

对于集合对象,Redis也可以配置阈值:

1
2
3
# redis.conf配置
# 集合对象中,使用整数集合的阈值
set-max-intset-entries 512 # 元素数量阈值
  • 使用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会指向一个embstrraw结构。
  • raw:如果输入的[value]为一个长度大于39字节的字符串,则该对象中,ptr指向的是一个sdshdr(简单动态字符串)结构。
  • embstr:如果输入的[value]为一个长度小于等于39字节的字符串,则该对象中,ptr指向的也是一个sdshdr,并将字符串编码encoding设为embstr
    • raw所使用的底层结构虽然相同(都是用的sdshdr),但是embstr在创建对象时,会创建一个连续的空间,将redisObject对象和sdshdr结构先后放在一起。因此,embstr的创建只需要分配一次内存,释放也只需要释放一次内存。
  • 如果输入的[value]为浮点数(可被long double类型标识),则该对象中,ptr指向的将是一个embstrraw(取决于该浮点数转化为字符串之后的长度)
    • 在读取这类对象的时候,程序会将字符串对象的字符串值转为浮点数值,经过某些操作后,再将其转回为字符串值并存入对象中。

编码转换

在某些情况下,字符串对象的编码会发生转换:

  • 原对象中存储的是int对象,但是对其操作时加入了一个字符串值,则会将该对象的编码变为raw
    • 这种情况即使在对象修改后长度小于等于39时也会发生。这是因为Redis对于每一个字符串类型的设计理念有着明确的分工。其中
      • int是一个只读的整型
      • embstr是一个只读的短字符串
    • 如果要将int修改为embstr,则会涉及到一些复杂的内存空间创建逻辑,且在代码层面上会使逻辑出现更多分支。
    • 对象修改之后也许会再次遭到修改,那么用raw这种支持更新的编码类型将更好。
  • 原对象中存储的是embstr对象,但是对其操作时加入了一个另一个值,则会将该对象编码变为raw
    • 原因在于,Redis没有为embstr编码的字符串编写任何修改程序,所以可以视为embstr对象是“只读”对象。因此,在需要对这类对象进行修改时,程序会将embstr修改为raw,然后进行修改。

列表对象 list

基本创建方式:RPUSH [key] [value1] [value2] ...

编码

  • ziplist:如果创建时给定的[value1] ... 使用的是ziplist编码(需满足ziplist基本要求和满足列表使用ziplist的阈值),则该对象中,ptr指向的是一个ziplist
  • linkedlist:如果创建时个定的[value1] ...使用的是linkedlist编码(未满足ziplist基本要求和超出了列表使用ziplist的阈值),则该对象中,ptr指向的是一个linkedlist
    • 需要注意的是,在linkedlist中,列表对象底层包含的是多个字符串sdshdr对象。

编码转换

之前提过Redis底层会自动基于用户输入的value值决定是否使用ziplist,这也是基于之前提过几个阈值:

  • 列表对象保存的所有字符串元素长度都小于阈值(默认为64)字节;
  • 列表对象保存的元素数量小于阈值(默认为512个);

所对应的在redis.conf配置文件中的配置条目为:

1
2
3
# 列表对象
list-max-ziplist-value 64 # 单个元素大小阈值(字节)
list-max-ziplist-entries 512 # 元素数量阈值

若是对象原本使用的是ziplist编码,在对对象进行操作导致其无法满足以上两个条件的任意一个时,对象的编码转换操作就会执行:将原本在ziplist中的元素转移到linkedlist中,并将编码转为linkedlist

  • 注意: 这一过程是不可逆的。这意味着一旦编码变为了linkedlist,便不会再变回ziplist。这是因为Redis考虑了防抖动机制和避免过多的内存空间重分配的情况。

哈希对象 hash

基本创建方式:HSET [key] [hash key1] [hash value1] ...

编码

  • ziplist:当对象保存的所有键值长度都小于配置阈值时,该对象的ptr指向的是一个ziplist,编码encoding将设为ziplist
    • ziplist内部结构为紧凑的键-值对。在添加一个新键值对时,哈希对象会先将键的压缩列表节点推到压缩列表尾部,然后再将值的压缩列表节点推到压缩列表尾部。
  • hashtable:当对象保存的键值中有任意一个不满足ziplist阈值,该对象的ptr将指向一个dict(字典结构),编码encoding将设为hashtable
  • 字典中的每一个键和值都是字符串对象StringObject,底层实现都是sdshdr

编码转换

哈希对象同样基于两个阈值来决定是否使用ziplist编码:

  • 哈希对象保存的所有键值对的键和值的字符串长度都小于阈值(默认64字节);
  • 哈希对象保存的键值对数量小于512个;

所对应的redis.conf配置文件中的配置条目为:

1
2
hash-max-ziplist-entries 512   # 字段数量阈值
hash-max-ziplist-value 64 # 字段值长度阈值

哈希对象通过某些操作导致无法满足这两个条件时,该哈希对象的编码将变为hashtable:原本保存在压缩列表里的所有键值对都将转移并保存到字典中

  • 注意: 与列表一样,此过程是不可逆的。不存在变成hashtable后又变回ziplist的情况。

集合对象 set

基本创建方式:SADD [key] [value1] [value2] ...

编码

  • intset:输入的[value]值满足集合对象使用intset的阈值时,该对象会采用intset作为编码,ptr将指向一个intset结构
  • hashtable:输入的[value]值没有满足集合对象使用的阈值时,该对象会采用hashtable作为编码,ptr将指向一个hashtable结构

编码转换

集合对象需要在同时满足以下两个条件时,使用intset编码:

  • 集合对象保存的所有元素都是整数值;
  • 集合对象保存的元素数量不超过设定阈值(默认512个);

所对应的redis.conf配置文件中的配置条目为:

1
set-max-intset-entries 512     # 元素数量阈值

对于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]。因此,可以通过使用这个跳表来执行范围型操作。
      • 字典: 维护着集合元素中所有成员与其分值的映射。键为成员,值为对应成员的分值。这样可以以O(1)O(1)的复杂度快速找到对应成员的分值。
    • 并用两个结构的动机是:

      • 充分利用跳表的特性来高效执行范围操作
      • 充分利用字典的特性来高效执行查找操作
      • 用两个结构来存储相同内容是否会导致数据重复而影响内存?不会,因为他们共享了元素的成员和分值。

编码转换

当有序集合对象可以同时满足一下两个条件时,对象是用ziplist编码:

  • 有序集合保存的元素数量小于阈值(默认为128个);
  • 有序集合保存的所有元素成员长度均小于阈值(默认64字节);

所对应在redis.conf配置文件中的配置条目为:

1
2
zset-max-ziplist-entries 128   # 元素数量阈值
zset-max-ziplist-value 64 # 元素值长度阈值

对于使用 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
2
3
4
5
typedef struct redisObject{
// ...
int refcount; // 引用计数
// ...
} robj;

机制与python很像:

  • 创建新对象时,引用计数值初始化为1
  • 对象被新程序使用时,将调用incrRefCount函数,使引用计数值增加1
  • 对象不在被一个程序使用时,将调用decrRefCount函数,是引用计数值减少1
  • 对象的引用计数为0时,对象所占用的内存将被释放

还有一个函数为resetRefCount,用于将对象的引用计数设置为0,但这一函数并不会释放对象内存,因为其通常在需要重新设置对象引用计数值时使用。

对象共享

Redis中使用引用计数器实现了对象共享的功能:假如之前创建了一个对象A且没有被内存回收,现在又要创建另一个对象B,且里面的元素与对象A的相同。这时候并不会真的为对象B的元素去创建一个新对象,而是将其直接与对象A的元素共享,并将其引用计数加一。

不过有如下内容需要 注意:

  • 共享对象的值只能是整数类型
  • 数据结构中嵌套了字符串对象的对象,也可以使用这些共享对象(也只能是整数类型)

共享对象的值只能是整数类型的解释为:

  • 验证整数值的字符串对象所需的复杂度为O(1)O(1)
  • 验证字符串值的字符串对象所需的复杂度为O(N)O(N)
  • 验证多个值(对象)的对象,比如列表或哈希,那么复杂度为O(N2)O(N^2)
  • 出现相同的字符串值和包含了多个值的对象的概率,比出现相同整数值的概率要小的多

综上所述,并没有必要去给字符串值等字符串对象设计对象共享机制,这样自己很容易让程序的复杂度变高许多。

对象的空转时长

1
2
3
4
5
typedef struct redisObject {
// ...
unsigned lru:32;
// ...
} robj;

redisObject结构还包含一个lru属性,该属性用于记录该对象最后一次被命令程序访问的时间。用这个时间,可以得到该对象的空转时间。在进行自动内存回收算法时将会用到该属性。

注意: 使用OBJECT IDLETIME [key]可以查看[key]对象的空转时长。但是这个命令并不会更新[key]对象的lru属性。


《Redis设计与实现》对象
http://example.com/2025/08/26/Redis学习/《Redis设计与实现》对象/
作者
Clain Chen
发布于
2025年8月26日
许可协议