redisObject通过统一的结构管理多种数据类型,并在底层根据数据特征动态选择最合适的编码方式,力求在性能与内存效率之间找到最佳平衡。


redisObject 结构体详解

redisObject 结构体(robj)定义如下(以 Redis 6.2.6 为例):

1
2
3
4
5
6
7
8
#define LRU_BITS 24
typedef struct redisObject {
unsigned type:4; // 对象类型
unsigned encoding:4; // 编码方式
unsigned lru:LRU_BITS; // 用于LRU或LFU信息
int refcount; // 引用计数
void *ptr; // 指向底层实现的数据结构的指针
} robj;
字段名 作用 备注
type 标识对象的 数据类型 例如 OBJ_STRING, OBJ_LIST, OBJ_SET, OBJ_ZSET, OBJ_HASH
encoding 标识对象底层使用的 编码(即具体数据结构) 例如 OBJ_ENCODING_INT, OBJ_ENCODING_EMBSTR, OBJ_ENCODING_RAW, OBJ_ENCODING_HT
lru 记录对象 最近一次被访问的时间访问频率 用于内存淘汰策略(LRU 或 LFU)
refcount 引用计数,用于 内存回收对象共享 为 0 时对象会被回收
ptr 指向实际数据的指针 对于 OBJ_ENCODING_INT 编码的字符串对象,ptr 直接存储整数值(而非指针)

类型 (type) 与编码 (encoding)

Redis 的数据类型 (type) 和其底层编码 (encoding) 是多对多的关系。这意味着同一种数据类型,在不同条件下会使用不同的底层数据结构实现。

下表概述了五种基本数据类型及其可能的编码方式:

数据类型 (type) 编码 (encoding) 底层数据结构 应用条件(可通过配置调整)
String OBJ_ENCODING_INT long 类型整数 字符串值可表示为 long 类型整数
OBJ_ENCODING_EMBSTR embstr 编码的 SDS 字符串长度 ≤ 44 字节
OBJ_ENCODING_RAW 简单动态字符串 (SDS) 字符串长度 > 44 字节
List OBJ_ENCODING_QUICKLIST 快速列表 (quicklist) Redis 3.2 之后列表的默认实现
Hash OBJ_ENCODING_ZIPLIST 压缩列表 (ziplist) 所有键和值的字符串长度均小于 hash-max-ziplist-value,且键值对数量小于 hash-max-ziplist-entries
OBJ_ENCODING_HT 字典 (dict) 不满足 ziplist 条件时
Set OBJ_ENCODING_INTSET 整数集合 (intset) 所有元素都是整数,且元素数量小于 set-max-intset-entries
OBJ_ENCODING_HT 字典 (dict) 不满足 intset 条件时
ZSet OBJ_ENCODING_ZIPLIST 压缩列表 (ziplist) 元素数量小于 zset-max-ziplist-entries,且每个元素的字符串长度均小于 zset-max-ziplist-value
OBJ_ENCODING_SKIPLIST 跳跃表和字典 (dict) 不满足 ziplist 条件时

这种 “对象类型-编码” 的设计让 Redis 能根据数据规模和特性,智能选择最节省内存或最高效访问的底层结构,并在条件变化时自动转换编码


Redis 如何利用 redisObject

redisObject 结构中的字段共同支撑了 Redis 的核心功能。

1. 类型检查与多态命令

  • 类型检查:执行命令前,Redis 会检查 redisObjecttype 属性。例如执行 LLEN 时,会检查操作的对象类型是否为 OBJ_LIST,否则返回类型错误。
  • 多态命令:许多命令的实现会根据 encoding 属性选择不同的处理函数。例如 LLEN 命令,无论是 quicklist 还是旧的 ziplist 编码的列表,都能正确返回长度,体现了基于编码的多态。

2. 内存回收与共享对象

  • 内存回收 (refcount):Redis 通过 refcount 实现引用计数垃圾回收。创建对象时 refcount 初始化为 1;被新程序使用时增一;不再使用时减一;当 refcount 为 0 时,回收对象占用的内存。

  • 共享对象 (refcount):为节省内存,Redis 在初始化时会创建 0 到 9999 的整数字符串对象池。当多个键需要存储相同的整数值时,会指向这个共享对象,并增加其 refcount

    注意:Redis 目前主要共享整数字符串对象,因为整数的比较操作时间复杂度是 O(1)。对于普通字符串,比较操作是 O(n),且重复可能性相对较低,使用共享对象可能得不偿失。

3. 空转时长与内存淘汰

  • lru 字段记录了对象最近一次被访问的时间(或 LFU 的访问频率信息)。通过 OBJECT IDLETIME key 命令可以查看对象的空转时长(不更新 lru 字段)。
  • 当 Redis 内存使用达到 maxmemory 限制,且淘汰策略设置为 volatile-lruallkeys-lru 时,会优先淘汰 lru 值最大(即最久未访问)的对象。

字符串对象创建示例

字符串对象的创建过程很好地体现了 Redis 的优化策略:

1
2
3
4
5
6
7
#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
robj *createStringObject(const char *ptr, size_t len) {
if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT) // 长度≤44字节
return createEmbeddedStringObject(ptr, len); // 使用embstr编码
else
return createRawStringObject(ptr, len); // 使用raw编码
}
  • OBJ_ENCODING_INT:若字符串表示的是整数,且值在 09999 之间,会直接使用共享对象。否则,ptr 字段会直接存储该整数值(void * 强制转换)。
  • OBJ_ENCODING_EMBSTR:对于短字符串(≤44字节),redisObjectsdshdr 结构体会被分配在一块连续的内存中,减少内存分配次数,提高缓存局部性。
  • OBJ_ENCODING_RAW:对于长字符串(>44字节),redisObjectsdshdr两次分配的不连续内存。

总结

redisObject 是 Redis 强大性能背后的无名英雄。它通过:

  • 统一管理:用单一结构体统一表示多种数据类型。
  • 灵活编码:根据数据特征动态选择最优底层结构,平衡内存与性能。
  • 透明转换:在数据变化时自动进行编码转换,对用户无感。
  • 高效内存管理:借助引用计数实现内存回收和有限的对象共享。
  • 支持淘汰策略:记录访问信息以支持 LRU/LFU 等内存淘汰策略。

理解 redisObject,能帮你更深入地洞察 Redis 的内部运作机制,知其然也更知其所以然。