土法炼钢兴趣小组的算法知识备份

【Redis / 缓存内核】对象模型与 SDS:redisObject · type · encoding

文章导航

分类入口
databasestorage
标签入口
#redis#redis-kernel#redisObject#sds#encoding#embstr#refcount#memory

目录

事件循环与 I/O 中,一条命令经 readQueryFromClient 解析后进入 processCommand,最终落到对 值对象 的读写。应用层看到的是 SET foo bar 里的字符串 bar,内核里却是 redisObject:同一逻辑类型可以对应多种物理编码,编码切换发生在命令路径上,而不是启动时一次性定死。

常见误区是把「Redis 的 string」等同于 C 的 char*:值里可以含 \0,长度由 SDS 元数据维护;type 只回答「这是 string / list / hash …」,具体怎么存 要看 encoding。本文按 Redis OSS 7.4.2 源码(src/object.csrc/sds.hsrc/server.h)钉住对象头、字符串编码与引用计数语义,并用 OBJECT 子命令说明如何在运行态核对编码。


一、redisObject:type 与 encoding 的分工

每个值在内存中对应一个 redisObjectrobj):

// src/server.h — Redis 7.4.2
struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS;  /* LRU 时钟或 LFU 计数,见第 8 篇 */
    int refcount;
    void *ptr;
};
字段 职责
type 逻辑类型:OBJ_STRINGOBJ_LISTOBJ_HASHOBJ_SETOBJ_ZSETOBJ_STREAM
encoding 同一 type 下的物理表示,如 string 的 RAW / EMBSTR / INT
ptr 指向 SDS、quicklist、dict、listpack 等具体结构
refcount 引用计数;减到 0 时按 type 释放 ptrzfree(o)
lru 淘汰策略用的近似访问信息(第 7–8 篇展开)

createObject 是新对象的统一入口:默认 encoding = OBJ_ENCODING_RAWrefcount = 1

// 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;
    return o;
}

键名在数据库 dict 里通常直接存 SDS 指针(见第 4 篇), 则几乎总是 robj*。命令实现因此分两层:先 lookupKeyRead / lookupKeyWrite 拿到 robj*,再按 type + encoding 分派到 t_string.ct_list.c 等。

flowchart TD
  cmd["processCommand"]
  key["db->dict key → SDS"]
  val["db->dict val → robj*"]
  cmd --> key
  cmd --> val
  val --> type["type: STRING/LIST/..."]
  val --> enc["encoding: RAW/quicklist/..."]
  enc --> ptr["ptr → concrete struct"]

二、字符串编码:RAW、EMBSTR、INT 与共享整数

OBJ_STRING 在 7.4 中有四种活跃编码(strEncoding 映射):

encoding 常量 含义
raw OBJ_ENCODING_RAW ptr 指向堆上 SDS
embstr OBJ_ENCODING_EMBSTR SDS 头与 robj 同块分配,只读语义
int OBJ_ENCODING_INT ptr(void*)(long)value,无堆串
(共享) shared.integers[] refcount = OBJ_SHARED_REFCOUNT,不可变

EMBSTR 阈值 在源码里写死为 44 字节——注释说明这是为了让「最大 embstr 对象」仍落在 jemalloc 64 字节 arena:

// src/object.c
#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
robj *createStringObject(const char *ptr, size_t len) {
    if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)
        return createEmbeddedStringObject(ptr,len);
    else
        return createRawStringObject(ptr,len);
}

createEmbeddedStringObject 一次 zmalloc(sizeof(robj)+sizeof(sdshdr8)+len+1),把 sdshdr8 紧挨在 robj 后面;o->ptr 指向内嵌 buffer。EMBSTR 不可原地扩容——若后续 APPEND 超长,会分配新 RAW 对象并替换。

整数字符串走 createStringObjectFromLongLongWithOptions

incrRefCount / decrRefCountOBJ_SHARED_REFCOUNT 与栈上 OBJ_STATIC_REFCOUNT 有特殊分支:共享对象引用计数不变,避免多线程读热整数时抢锁(IO threads 只搬运缓冲,命令仍在主线程执行,见第 2 篇)。


三、SDS:头部在指针前、二进制安全

SDS(Simple Dynamic String)在 Redis 里用了十多年,设计目标是:O(1) 取长度二进制安全预分配减少 realloc。对外类型是 typedef char *sds,但有效元数据在 s[-1]flags 字节及之前的 header。

7.4 支持五档头部(sds.h):

类型 长度上限(量级) 头部大小
type 5 31(仅嵌入 flags) 1 字节 flags
type 8 255 sdshdr8
type 16 64K sdshdr16
type 32 4G sdshdr32
type 64 极大 sdshdr64

sdslen 通过 flags & SDS_TYPE_MASK 分支读取 lensdsavail 返回 alloc - len。这与 C 字符串「遇 \0 结束」根本不同:Redis 协议和 RDB 都按显式长度传输,值中间出现 0x00 合法。

分配布局(概念):

[ sdshdr{8,16,...} | buf ... | '\0' ]  ← s 指向 buf
                              ↑
                         flags 在 s[-1]

sdsnewlensdscatlen 等在长度增长时可能升级 header 类型(例如从 type 8 升到 type 16)。键名、普通字符串值、跳表节点里的 member 名都复用同一套 SDS API(src/sds.c)。


四、OBJECT ENCODING 与运行态核对

OBJECT 是只读探针,不修改编码。OBJECT ENCODING key值对象调用 strEncoding(o->encoding)

// src/object.c — objectCommand 片段
} else if (!strcasecmp(c->argv[1]->ptr,"encoding") && c->argc == 3) {
    if ((o = objectCommandLookupOrReply(c,c->argv[2],shared.null[c->resp])) == NULL) return;
    addReplyBulkCString(c,strEncoding(o->encoding));
}

其它子命令:

注意OBJECT ENCODING 报告的是的 encoding。对 SET k vk 的存储不在此命令路径里——键没有独立 robj 包装。核对键名 SDS 需结合 MEMORY USAGE(采样估算)或调试器看 dict 条目。

本环境未部署 redis-server,下文不粘贴命令输出;读者可在本地执行:

redis-cli SET small hello
redis-cli OBJECT ENCODING small
redis-cli SET num 42
redis-cli OBJECT ENCODING num

预期:helloembstr(长度 ≤ 44),42int 或命中共享整数池时的共享对象(REFCOUNT 极大)。

MEMORY USAGE keyobject.c / memoryCommand)对嵌套结构递归采样,适合粗看 list/hash 是否已升级为 quicklist / hashtable(第 5 篇)。


五、学术谱系与工程争论

SDS 谱系:动态字符串并非 Redis 独创;Antirez 在 2006 年前后发布的 SDS 库把「长度 + 分配容量 + 类型分档」固化成可嵌入服务器的 API。相较 C 字符串,论文级对比点在 append 摊销二进制语义;Redis 把 SDS 同时用于键、值、协议缓冲,形成单一分配器路径(zmalloc / jemalloc)。

多编码争论redisObjecttype + encoding 双字段是典型 存储多态——小数据用 EMBSTR / listpack,大数据用 RAW / quicklist / skiplist。收益是内存;代价是:

  1. 代码路径爆炸:每种命令要处理多种 encoding(zsetAdd 同时分支 listpack 与 skiplist,见第 6 篇)。
  2. 升级不可逆或单向:小集合升到大结构后很少自动缩回,内存碎片与「逻辑删除后仍占大块」是运维常见痛点(第 15 篇)。
  3. 可观测性依赖:没有 OBJECT ENCODING,线上很难解释「同样 100 个 key 为何内存差 3 倍」。

社区里没有「应砍掉 encoding 改统一列存」的共识论文;工业界更常见的反驳是 工作负载偏斜——大量短 key、小 hash 时紧凑编码的收益在 Facebook Memcache 类报告(NSDI’13)里已有「小对象占比」证据,但数值不能跨系统直接比。

开放问题:模块(OBJ_MODULE)与 JSON 等扩展类型如何在统一 robj 框架下保持 encoding 可枚举,仍是 Redis / Valkey 生态的分叉点之一(第 16 篇一句带过)。


六、常见误解

误解 事实
Redis string 以 \0 结尾定长 长度由 SDS len 决定;\0 只是 buf 末尾的 C 兼容习惯
type 变了才会换存储 同 type 下 encoding 可随写入升级(如 hash listpack → hashtable)
OBJECT ENCODING 看的是 key 只看;键名无 robj 包装
EMBSTR 可原地 APPEND 任意长 超限会复制为新 RAW SDS,旧 embstr 释放
refcount 等于「有几个 key 指向它」 引用计数包含命令临时引用、共享对象、子结构内部指针;共享整数永不减到 0

七、三句话小结

  1. 在内存里是 redisObjecttype 定逻辑种类,encoding 定物理布局,ptr 指向 SDS / quicklist / dict 等具体结构。
  2. 字符串 优先走 EMBSTR(≤44B 同块分配)、整型 INT、共享整数池,否则 RAW SDS;SDS 头部在指针前,二进制安全。
  3. OBJECT ENCODING 是核对值编码的 A 级探针;键名是 dict 里的裸 SDS,别和值的 robj 混谈。

参考资料

A 级(规范 / 源码)

B 级(设计笔记)

核心论文(机制谱系)


返回 系列目录 | 上一篇:事件循环与 I/O | 下一篇:Dict 与渐进 rehash

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。


By .