redisObject
redisObject 源码分析(对象系统,把编码串起来)
redisObject 解决的问题:Redis 对外只有 String/List/Set/ZSet/Hash 等几种类型,对内每种类型却有多套底层结构(省内存的紧凑结构 + 保复杂度的专用结构)。
robj 是包在所有 value 外面的统一外壳,用 type 记对外类型、encoding 记当前底层实现,让同一类型能随数据规模自动换编码,而命令层不感知。
类比 Java:type 是接口(List),encoding 是实现类(ArrayList/LinkedList),robj 就是那个多态引用,只是切换发生在运行时、按数据量自动。
注意版本:网上教程和 7.x 源码里 redisObject 定义在 server.h,refcount 是 int。
本地仓是 unstable(8.4 开发线),结构体已挪进独立的 object.h,refcount 压成 23 位位域,腾出的位给了新字段 iskvobj/metabits——为了把 key 和过期时间直接内嵌到对象里。
讲 7.x 经典版没问题,但要知道新版已经长这样。
代码块收起展开
// 基于本地 Redis 仓 (unstable, 8.4 开发线), src/server.h + src/object.h(7.x 时代这些定义都在 server.h)
// ---- src/server.h:type,TYPE 命令看到的就是它 ----
#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. */
#define OBJ_SHARED_INTEGERS 10000 // 共享整数池的上界:0~9999
// ---- src/object.h:encoding,OBJECT ENCODING 命令看到的就是它 ----
#define OBJ_ENCODING_RAW 0 /* Raw representation */
#define OBJ_ENCODING_INT 1 /* Encoded as integer */ // 值直接塞 ptr 字段,零额外分配
#define OBJ_ENCODING_HT 2 /* Encoded as hash table */
#define OBJ_ENCODING_ZIPMAP 3 /* No longer used: old hash encoding. */
#define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */ // 废弃编码不复用编号,保 RDB 兼容
#define OBJ_ENCODING_ZIPLIST 5 /* No longer used: old list/hash/zset encoding. */ // 7.0 起被 listpack 全面取代
#define OBJ_ENCODING_INTSET 6 /* Encoded as intset */
#define OBJ_ENCODING_SKIPLIST 7 /* Encoded as skiplist */ // 实为 skiplist+dict 双结构
#define OBJ_ENCODING_EMBSTR 8 /* Embedded sds string encoding */
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of listpacks */
#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */
#define OBJ_ENCODING_LISTPACK 11 /* Encoded as a listpack */
#define OBJ_ENCODING_LISTPACK_EX 12 /* Encoded as listpack, extended with metadata */ // 带 field TTL 的 hash 用
#define LRU_BITS 24
#define OBJ_REFCOUNT_BITS 23
#define OBJ_SHARED_REFCOUNT ((1 << OBJ_REFCOUNT_BITS) - 1) /* Global object never destroyed. */ // 共享对象的"永生"标记
#define OBJ_STATIC_REFCOUNT ((1 << OBJ_REFCOUNT_BITS) - 2) /* Object allocated in the stack. */
#define OBJ_FIRST_SPECIAL_REFCOUNT OBJ_STATIC_REFCOUNT
struct redisObject {
unsigned type:4; // 对外类型
unsigned encoding:4; // 对内实现,同一 type 可换多种 encoding
unsigned refcount : OBJ_REFCOUNT_BITS; // 7.x 是 32 位 int,压成 23 位为下面两个新字段腾位
unsigned iskvobj : 1; /* 1 if this struct serves as a kvobj base */ // 8.x 新增:key 内嵌在本对象尾部,省 dictEntry 的指针跳转
/* metabits and lru are Relevant only when iskvobj is set: */
unsigned metabits :8; /* Bitmap of metadata (+expiry) attached to this kvobj */ // 过期时间等元数据也内嵌(放在对象前面)
unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits access time). */ // 一字段两用,按 maxmemory-policy 解释
void *ptr; // 指向真正的底层结构 sds/quicklist/dict/intset/...;INT 编码时存的是值本身
};
/* robj - General purpose redis object */
typedef struct redisObject robj;type 与 encoding 的对应关系(重点对应关系,一张表理清;箭头方向即超阈值后的转换方向):
OBJ_STRING:INT(≤20 位十进制整数)/ EMBSTR(≤44 字节)/ RAW(长串)OBJ_LIST:LISTPACK → QUICKLISTOBJ_SET:INTSET(全整数)→ LISTPACK(出现非整数但仍少)→ HTOBJ_ZSET:LISTPACK → SKIPLISTOBJ_HASH:LISTPACK / LISTPACK_EX → HT
集合类的转换阈值由 *-max-listpack-entries / *-max-listpack-value 等配置控制,转换代码在各类型自己的命令文件里(如 t_hash.c 的 hashTypeConvert),object.c 只负责字符串的编码压缩。转换全部单向,不会因为元素变少而缩回去。
// 基于本地 Redis 仓 (unstable, 8.4 开发线), src/object.c —— 对象创建
robj *createObject(int type, void *ptr) {
robj *o = zmalloc(sizeof(*o));
o->type = type;
o->encoding = OBJ_ENCODING_RAW;
o->ptr = ptr;
o->refcount = 1;
o->lru = 0; // LRU/LFU 不在这里初始化,对象真正进 db 时才由 initObjectLRUOrLFU 补
o->iskvobj = 0;
o->metabits = 0;
return o;
}
robj *createRawStringObject(const char *ptr, size_t len) {
return createObject(OBJ_STRING, sdsnewlen(ptr,len)); // RAW:robj 和 SDS 各自 malloc,共两次分配、两次 free
}
robj *createEmbeddedStringObject(const char *val_ptr, size_t val_len) {
/* Calculate size for embedded value (always SDS_TYPE_8) */
size_t val_sds_size = sdsReqSize(val_len, SDS_TYPE_8);
/* Allocate object memory */
size_t bufsize = 0;
robj *o = zmalloc_usable(sizeof(robj) + val_sds_size, &bufsize); // EMBSTR:robj+SDS 连体,一次 malloc
o->type = OBJ_STRING;
o->encoding = OBJ_ENCODING_EMBSTR;
o->refcount = 1;
o->lru = 0;
o->metabits = 0;
o->iskvobj = 0;
/* The memory after the struct where we embedded data. */
char *data = (char *)(o + 1); // 字符串紧跟在结构体后面,头和数据大概率同一 cache line
size_t remaining_size = bufsize - (data - (char *)(void *)o);
o->ptr = sdsnewplacement(data, remaining_size, SDS_TYPE_8, val_ptr, val_len); // 就地构造 SDS:没法单独扩容,所以 embstr 天生只读
return o;
}
/* The current limit of 44 is chosen so that the biggest string object
* we allocate as EMBSTR will still fit into the 64 byte arena of jemalloc. */
#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44 // 44 = 64(jemalloc 最小 size class) - 16(robj头) - 3(sdshdr8头) - 1('\0')
robj *createStringObject(const char *ptr, size_t len) {
if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)
return createEmbeddedStringObject(ptr,len);
else
return createRawStringObject(ptr,len);
}
#define LL2STROBJ_AUTO 0 /* automatically create the optimal string object */
// ...
robj *createStringObjectFromLongLongWithOptions(long long value, int flag) {
robj *o;
if (value >= 0 && value < OBJ_SHARED_INTEGERS && flag == LL2STROBJ_AUTO) {
o = shared.integers[value]; // 0~9999 启动时预建好全局复用;共享对象计数不可变,这里连 refcount 都不用动
} else {
if ((value >= LONG_MIN && value <= LONG_MAX) && flag != LL2STROBJ_NO_INT_ENC) {
o = createObject(OBJ_STRING, NULL);
o->encoding = OBJ_ENCODING_INT;
o->ptr = (void*)((long)value); // 整数值伪装成指针存进 ptr,字符串本体的分配彻底省掉
} else {
char buf[LONG_STR_SIZE];
int len = ll2string(buf, sizeof(buf), value);
o = createStringObject(buf, len); // 超出 long 范围(如 LLONG_MAX 在 32 位下)退回按字符串存
}
}
return o;
}// 基于本地 Redis 仓 (unstable, 8.4 开发线), src/object.c —— 编码压缩与引用计数
robj *tryObjectEncodingEx(robj *o, int try_trim) {
long value;
sds s = o->ptr;
size_t len;
serverAssertWithInfo(NULL,o,o->type == OBJ_STRING); // 只处理字符串;集合类的编码转换归各类型命令管
if (!sdsEncodedObject(o)) return o; // 已是 INT 编码,没得再压
if (o->refcount > 1) return o; // 坑:被共享的对象不能原地改编码,别处还拿着旧指针/旧编码的假设
/* Check if we can represent this string as a long integer.
* Note that we are sure that a string larger than 20 chars is not
* representable as a 32 nor 64 bit integer. */
len = sdslen(s);
if (len <= 20 && string2l(s,len,&value)) {
/* This object is encodable as a long. */
if (o->encoding == OBJ_ENCODING_RAW) {
sdsfree(o->ptr);
o->encoding = OBJ_ENCODING_INT; // RAW 可原地改:释放 SDS,ptr 换存值
o->ptr = (void*) value;
return o;
} else if (o->encoding == OBJ_ENCODING_EMBSTR) {
decrRefCount(o); // embstr 与 robj 连体,无法只释放字符串部分,只能整个换新对象
return createStringObjectFromLongLongForValue(value); // 走共享整数/INT;LRU/LFU 淘汰策略下会避开共享对象
}
}
if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT) {
robj *emb;
if (o->encoding == OBJ_ENCODING_EMBSTR) return o;
emb = createEmbeddedStringObject(s,sdslen(s)); // 短 RAW 串转 embstr(客户端解析大参数时可能先建成 RAW)
decrRefCount(o);
return emb;
}
if (try_trim)
trimStringObjectIfNeeded(o, 0); // 压不动了,至少把 SDS 尾部超 10% 的预留空间还给分配器
/* Return the original object. */
return o;
}
robj *tryObjectEncoding(robj *o) {
return tryObjectEncodingEx(o, 1);
}
robj *makeObjectShared(robj *o) {
serverAssert(o->refcount == 1);
o->refcount = OBJ_SHARED_REFCOUNT; // 打上"永生"标记后 incr/decr 都不再碰它,跨线程访问无需加锁
return o;
}
void incrRefCount(robj *o) {
if (o->refcount < OBJ_FIRST_SPECIAL_REFCOUNT - 1) {
o->refcount++; // 命令在主线程串行执行,普通 ++ 就够,不需要原子操作
} else {
if (o->refcount == OBJ_SHARED_REFCOUNT) {
/* Nothing to do: this refcount is immutable. */
} else if (o->refcount == OBJ_STATIC_REFCOUNT) {
serverPanic("You tried to retain an object allocated in the stack");
} else {
serverPanic("You tried to retain an object with maximum refcount"); // 23 位上限的防御检查,7.x int 版没有
}
}
}
void decrRefCount(robj *o) {
if (o->refcount == OBJ_SHARED_REFCOUNT)
return; /* Nothing to do: this refcount is immutable. */
if (unlikely(o->refcount <= 0)) {
serverPanic("illegal decrRefCount for object with: type %u, encoding %u, refcount %d",
o->type, o->encoding, o->refcount);
}
if (--(o->refcount) == 0) {
void *alloc = o;
if (o->iskvobj) {
/* eval real allocation pointer */
alloc = kvobjGetAllocPtr(o); // kvobj 的元数据前缀在对象地址之前,free 的起点要往前挪
// ...
}
if (o->ptr != NULL) {
switch(o->type) {
case OBJ_STRING: freeStringObject(o); break; // 按 type 分发析构,C 里手写的"虚函数表"
case OBJ_LIST: freeListObject(o); break;
case OBJ_SET: freeSetObject(o); break;
case OBJ_ZSET: freeZsetObject(o); break;
case OBJ_HASH: freeHashObject(o); break;
case OBJ_MODULE: freeModuleObject(o); break;
case OBJ_STREAM: freeStreamObject(o); break;
default: serverPanic("Unknown object type"); break;
}
}
zfree(alloc); // free 的是 alloc 不是 o:连元数据前缀一起还
}
}原理串讲
拿 SET count 123 走一遍完整链路。协议解析阶段,参数就已经被包成 robj:networking 层对每个参数调 createStringObject,“123” 只有 3 字节,走 createEmbeddedStringObject,robj 头和 SDS 一次 zmalloc_usable 分配在连续 64 字节内。
到了 t_string.c 的 setGenericCommand,存库前先调 tryObjectEncoding 压缩 value:sdsEncodedObject 确认还是字符串形态,refcount > 1 确认没被共享,然后 string2l 发现 “123” 能解析成 long。
因为当前是 embstr(连体块没法只换字符串部分),走 decrRefCount 整个丢弃,转手调 createStringObjectFromLongLongForValue:123 落在 OBJ_SHARED_INTEGERS(10000)以内,直接返回启动时预建的 shared.integers[123],一个字节的新内存都不分配。
最后对象进 db,8.x 里还会经 kvobjSet 把 key 字符串内嵌到 value 对象尾部,连 dictEntry 里单独的 key 指针都省了。
读路径同理:GET 拿到 robj 后按 encoding 分发,INT 编码临时 ll2string 转回字符串输出(getDecodedObject)。
第一处为什么:EMBSTR 的分界为什么恰好是 44?jemalloc 按 size class 分配,64 字节是常用的小对象档位。
robj 头 16 字节 + sdshdr8 头 3 字节 + 结尾 \0 1 字节 = 20 字节开销,64 - 20 = 44。
也就是说 44 这个数字是”倒推”出来的:目标是让最大的 embstr 对象恰好塞满一个 64 字节 arena,一次分配、一次释放、一条 cache line 读完。
超过就退回 RAW 两段式,因为 RAW 的 SDS 可以独立 realloc,APPEND/SETRANGE 才有原地扩容的余地。
第二处为什么:tryObjectEncodingEx 开头为什么见到 refcount > 1 就放弃?改编码是”析构旧结构 + 换 ptr + 改 encoding”的原地手术,而引用计数大于 1 意味着这个对象同时活在别处(比如另一个 key、或某个正在执行的命令栈上),那边还按旧编码解释 ptr。
原地一改,别处立刻悬空。同理,makeObjectShared 把共享对象的 refcount 钉死在 OBJ_SHARED_REFCOUNT,incrRefCount/decrRefCount 见到这个值直接跳过读写。
计数永不变化,就没有数据竞争,I/O 线程、模块线程访问共享小整数都不用锁,这是单线程模型之外少数需要考虑并发的点,Redis 用”不可变”而非”加锁”解决。
第三处为什么:转换为什么单向?元素从 200 掉回 10,Hash 也不会从 HT 缩回 listpack。
判断”该不该缩”每次写操作都要做,而阈值附近的抖动会导致 listpack 与 HT 之间反复整体重建,代价远超过紧凑结构省下的那点内存。
单向转换等于把复杂度上界一次性买断,缩回去的收益等 key 被删除重建时自然兑现。
设计取舍
- 位域抠到极致:type/encoding/refcount/iskvobj/metabits/lru 全部挤进 8 字节,加 ptr 恰好 16 字节头。上亿 key 的场景下,头部每省 1 字节都是百 MB 级收益。
- embstr 只读:任何修改(APPEND/SETRANGE/GETSET 后再改)都先转 RAW 再动手,改完哪怕又小于 44 也不会转回 embstr。
- 共享整数与淘汰策略冲突:
lru字段存在对象里,共享对象只有一份lru,没法为每个 key 单独记热度。
所以 maxmemory-policy 配了 LRU/LFU 时,createStringObjectFromLongLongForValue会放弃共享、给每个 value 单独建 INT 对象。 OBJECT ENCODING key可以直接观察本文全部结论,OBJECT REFCOUNT/OBJECT IDLETIME分别看计数和空转时间,动手验证比背表可靠。- 8.x 的 kvobj 改造(
iskvobj/metabits)把 key 和过期时间内嵌进 value 对象,砍掉 dictEntry 的指针跳转和独立 expires 字典的开销,是近年对象系统最大的一次结构变更,聊到 7.x 经典版之余可以当加分项。