Redis源码分析之robj篇

Redis源码分析之robj篇

在 Redis 中,一个 database 内的这个映射关系是用一个 dict 来维护的(ht[0])。dict 的 key 固定用一种数据结构来表达就够了,即动态字符串 sds。而 value 则比较复杂,为了在同一个 dict 内能够存储不同类型的 value,需要一个通用的数据结构,这个通用的数据结构就是 robj(全称 RedisObject )。

1
2
3
4
5
6
7
8
9
10
11
12
13
#define LRU_BITS 24

// redis 键值对中的 value 结构体,头占 16 字节,ptr 指针指向真正的数据
typedef struct redisObject {
unsigned type: 4; // 4 bit 数据类型 string、list、set
unsigned encoding: 4; // 4 bit 编码方式 embStr、raw、ziplist
unsigned lru: LRU_BITS; // 24 bit LRU 时间
/* LRU time (relative to global lru_clock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits access time). */
int refcount; // 4 字节 引用次数,主要针对共享对象
void *ptr; // 8 字节 指向具体实现的指针
} robj;

首先解释下这5个字段的含义:

  • type 数据类型:string、list、hash…
1
2
3
4
5
6
7
8
9
/* The actual Redis Object */
#define OBJ_STRING 0 /* String object. */
#define OBJ_LIST 1 /* List object. */
#define OBJ_SET 2 /* Set object. */
#define OBJ_ZSET 3 /* Sorted set object. */
#define OBJ_HASH 4 /* Hash object. */

#define OBJ_MODULE 5 /* Module object. */
#define OBJ_STREAM 6 /* Stream object. */
  • encoding 编码方式:比如 string 就有 embstr 和 raw 编码方式;
1
2
3
4
5
6
7
8
9
10
11
#define OBJ_ENCODING_RAW 0     /* Raw representation */
#define OBJ_ENCODING_INT 1 /* Encoded as integer */
#define OBJ_ENCODING_HT 2 /* Encoded as hash table */
#define OBJ_ENCODING_ZIPMAP 3 /* Encoded as zipmap */
#define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define OBJ_ENCODING_INTSET 6 /* Encoded as intset */
#define OBJ_ENCODING_SKIPLIST 7 /* Encoded as skiplist */
#define OBJ_ENCODING_EMBSTR 8 /* Embedded sds string encoding */
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */
  • lru: ‌LRU(Least Recently Used)时间,在 redis 淘汰数据时会用;
  • refcount: 引用计数。它允许 robj 对象在某些情况下被共享,例如 redis 在启动时对于常见的响应指令(ok、error),错误信息,0-9999 的数字对象都进行了创建并复用;
  • ptr: 指针,指向真正的值。

接着分析下这个结构体的定义方式

1
unsigned type: 4

这是 C 语言里的位域定义方法,表示该字段所占的比特数(bit)

最后我们分析可以知道,一个这样的 redisObject 需要占用 4bit + 4bit + 24bit + 4B +8B 即 16 字节空间,这对我们使用 redis 有没有什么启发呢?

  • 如果我要存一个很短的字符串, 字符串可能不到16字节,但为了描述这个字符串的头结构就占了16字节,内存利用率是不是低了?
  • 如果我要存一个数字,8字节就能搞定,还有必要让 ptr 指针去指向一个真实存储这个数字的空间吗?

另外,目前的机器基本都是 64 位架构,通常处理器的缓存行大小是 64 字节(64B),这意味着每次从内存加载数据到缓存时,最小的单位是 64 字节。即使你只需要其中的一部分数据,剩余的部分也会被一并加载到缓存中。

Redis 中的创建的很多字符串,都会采用 SDS8 的结构(SDS5 无法记录有效容量,对后续拓展不友好),如果 SDS8 的字符串 + redisObject 的头大小,恰好能满足 64B,意味着就不用再去内存中取数据了。

这就是 Redis String 类型 embStr 编码方式:

1
2
3
4
5
6
7
8
9
10
11
// <= 44 字节就使用 embStr 的 encoding 类型,否则使用 raw 编码类型
#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44

// 创建字符串对象的函数
robj *createStringObject(const char *ptr, size_t len) {
// 字符串少于44字节,就 embStr 类型编码,否则 raw 类型编码
if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)
return createEmbeddedStringObject(ptr, len);
else
return createRawStringObject(ptr, len);
}
1
2
3
4
5
6
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; // 现有长度 1 字节
uint8_t alloc; // 字符数组的空间长度(不包括结构体头和字符数组最后'\0'的一字节) 1 字节
unsigned char flags; // 标识位 3 bit 用来标识类型,5 bit 暂时无用 1 字节
char buf[]; // 真正存放字符串的数组,以 '\0' 结尾,注意其没有指明长度,这是一种特殊写法,柔性数组,初始化分配时不占用内存空间
};

指的一提的是,即使刚开始我们创建的是一个 embStr 编码的字符串,只要对其进行 append 操作,编码方式就会变为 raw,原因是变动会执行下面这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
void appendCommand(client *c) {
size_t totlen;
robj *o, *append;

o = lookupKeyWrite(c->db, c->argv[1]);
if (o == NULL) {
/* Create the key */
c->argv[2] = tryObjectEncoding(c->argv[2]);
dbAdd(c->db, c->argv[1], c->argv[2]);
incrRefCount(c->argv[2]);
totlen = stringObjectLen(c->argv[2]);
} else {
/* Key exists, check type */
if (checkType(c, o, OBJ_STRING))
return;

/* "append" is an argument, so always an sds */
append = c->argv[2];
totlen = stringObjectLen(o) + sdslen(append->ptr);
if (checkStringLength(c, totlen) != C_OK)
return;

/* Append the value */
// 这里在追加值的时候 会把新生成的 str 转为 raw 编码
o = dbUnshareStringValue(c->db, c->argv[1], o);
o->ptr = sdscatlen(o->ptr, append->ptr, sdslen(append->ptr));
totlen = sdslen(o->ptr);
}
signalModifiedKey(c->db, c->argv[1]);
notifyKeyspaceEvent(NOTIFY_STRING, "append", c->argv[1], c->db->id);
server.dirty++;
addReplyLongLong(c, totlen);
}

robj *dbUnshareStringValue(redisDb *db, robj *key, robj *o) {
serverAssert(o->type == OBJ_STRING);
// 引用计数不为1 或 之前不是 raw 编码
if (o->refcount != 1 || o->encoding != OBJ_ENCODING_RAW) {
robj *decoded = getDecodedObject(o);
// 创建 raw 编码的对象
o = createRawStringObject(decoded->ptr, sdslen(decoded->ptr));
decrRefCount(decoded);
dbOverwrite(db, key, o);
}
return o;
}

关于 SDS 的 44 字节和 append 操作后会变为 raw 编码其实在上一篇 Redis源码分析之sds篇 中已经提过了~

Robj 在创建字符串对象时,还会尝试把字符串转为 64 位可表示的 long,如果能转成功:

  • 该值处于 0-9999 且不运行 LRU(如果运行 LRU,会影响对象的共享),会返回一个共享对象(前面提到 Redis 会共享 0-9999 的 Robj 对象);
  • 其他情况下 ptr 指针字段直接存成这个 long 型的值。

只要该值可以被转为 64 位可表示的 long,最终的表示形式就是 type=OBJ_STRING, ecoding=OBJ_ENCODING_INT 的一个 string robj 对象。

Robj 不只是为值提供一个统一的表示方式,还允许同一类型的数据采用不同的内部表示,从而在某些情况下尽量节省内存,同时支持对象共享和引用计数。当对象被共享的时候,只占用一份内存拷贝,进一步节省内存。


Redis源码分析之robj篇
https://zhuwenjie0716.github.io/2026/05/14/Redis源码分析之robj篇/
作者
Wenjie Zhu
发布于
2026年5月14日
许可协议