在 事件循环与
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.c、src/sds.h、src/server.h)钉住对象头、字符串编码与引用计数语义,并用
OBJECT 子命令说明如何在运行态核对编码。
一、redisObject:type 与 encoding 的分工
每个值在内存中对应一个
redisObject(robj):
// 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_STRING、OBJ_LIST、OBJ_HASH、OBJ_SET、OBJ_ZSET、OBJ_STREAM
等 |
encoding |
同一 type 下的物理表示,如 string 的
RAW / EMBSTR /
INT |
ptr |
指向 SDS、quicklist、dict、listpack 等具体结构 |
refcount |
引用计数;减到 0 时按 type 释放
ptr 并 zfree(o) |
lru |
淘汰策略用的近似访问信息(第 7–8 篇展开) |
createObject 是新对象的统一入口:默认
encoding = OBJ_ENCODING_RAW,refcount = 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.c、t_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:
- \(0 \leq value <
10000\)(
OBJ_SHARED_INTEGERS)且允许共享 → 返回shared.integers[value],refcount为INT_MAX,永不释放。 - 否则若落在
long范围 →encoding = INT,ptr强转为整数。 - 更大或需要非共享 → 格式化成 SDS(可能 EMBSTR/RAW)。
incrRefCount / decrRefCount 对
OBJ_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 分支读取
len;sdsavail 返回
alloc - len。这与 C 字符串「遇 \0
结束」根本不同:Redis 协议和 RDB
都按显式长度传输,值中间出现
0x00 合法。
分配布局(概念):
[ sdshdr{8,16,...} | buf ... | '\0' ] ← s 指向 buf
↑
flags 在 s[-1]
sdsnewlen、sdscatlen
等在长度增长时可能升级 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 REFCOUNT:值对象引用计数(共享整数返回INT_MAX量级)。OBJECT IDLETIME/OBJECT FREQ:依赖maxmemory-policy是否 LRU/LFU(第 8 篇)。
注意:OBJECT ENCODING
报告的是值的 encoding。对
SET k v,k
的存储不在此命令路径里——键没有独立 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预期:hello 为 embstr(长度 ≤
44),42 为 int
或命中共享整数池时的共享对象(REFCOUNT
极大)。
MEMORY USAGE key(object.c /
memoryCommand)对嵌套结构递归采样,适合粗看
list/hash 是否已升级为 quicklist / hashtable(第 5
篇)。
五、学术谱系与工程争论
SDS 谱系:动态字符串并非 Redis
独创;Antirez 在 2006 年前后发布的 SDS 库把「长度 + 分配容量
+ 类型分档」固化成可嵌入服务器的 API。相较 C
字符串,论文级对比点在 append 摊销 与
二进制语义;Redis 把 SDS
同时用于键、值、协议缓冲,形成单一分配器路径(zmalloc
/ jemalloc)。
多编码争论:redisObject 的
type + encoding 双字段是典型
存储多态——小数据用 EMBSTR /
listpack,大数据用 RAW / quicklist /
skiplist。收益是内存;代价是:
- 代码路径爆炸:每种命令要处理多种
encoding(
zsetAdd同时分支 listpack 与 skiplist,见第 6 篇)。 - 升级不可逆或单向:小集合升到大结构后很少自动缩回,内存碎片与「逻辑删除后仍占大块」是运维常见痛点(第 15 篇)。
- 可观测性依赖:没有
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 |
七、三句话小结
- 值 在内存里是
redisObject:type定逻辑种类,encoding定物理布局,ptr指向 SDS / quicklist / dict 等具体结构。 - 字符串 优先走 EMBSTR(≤44B
同块分配)、整型
INT、共享整数池,否则 RAW SDS;SDS 头部在指针前,二进制安全。 OBJECT ENCODING是核对值编码的 A 级探针;键名是 dict 里的裸 SDS,别和值的robj混谈。
参考资料
A 级(规范 / 源码)
- Redis OSS
7.4.2:
src/server.h(redisObject、OBJ_ENCODING_*)、src/object.c(createStringObject、objectCommand、strEncoding)、src/sds.h/src/sds.c - Redis 命令文档:OBJECT
B 级(设计笔记)
- Antirez,SDS 库与 Redis 字符串设计(维护者博客,解释 EMBSTR 与 jemalloc arena 权衡)
- Redis 官方文档:Memory optimization(紧凑编码与配置项总览,与第 5 篇交叉)
核心论文(机制谱系)
- Pugh, W., Skip Lists: A Probabilistic Alternative to Balanced Trees, CACM 1990 — 第 6 篇 ZSet 跳表直接引用;本篇仅作「随机化结构 vs 确定性平衡」背景
返回 系列目录 | 上一篇:事件循环与 I/O | 下一篇:Dict 与渐进 rehash
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【Redis / 缓存内核】紧凑编码:listpack · quicklist · intset
Redis 7.4/8.x 如何用 listpack、quicklist、intset 为小集合省内存:ziplist 退场原因、各 type 的 encoding 升级阈值与配置项默认值,以及紧凑表示在 CPU 与内存之间的工程争论。
【Redis / 缓存内核】缓存服务器全景:HashMap · Memcached · Redis · 嵌入 KV
定位进程内 HashMap、Memcached、Redis 与 RocksDB 嵌入 KV 在内存缓存栈中的生态位;划分与 architecture/17、storage/66、linux/redis-source 的分工,并给出本系列 16 篇阅读地图。
【Redis / 缓存内核】事件循环与 I/O:ae · 客户端缓冲 · IO threads
按 Redis OSS 7.4/8.x 源码拆解 aeEventLoop 文件/时间事件、beforeSleep 钩子与客户端 querybuf/reply 路径,并精确说明 IO threads 只卸载读写与解析、命令仍在主线程执行。
【Redis / 缓存内核】Dict 与渐进 rehash:双 ht 与摊销扩容
从 Redis 7.4/8.x 的 dict 源码拆解链式哈希、双表渐进 rehash、rehashidx 步进与 DICT_RESIZE_AVOID 在 fork 期的行为,并对照站内哈希表内部文章说明与教科书一次性扩容的差异。