struct redisObject { unsigned type:4; // [0-3 bit] 对象类型 (如 String) unsigned encoding:4; // [4-7 bit] 编码方式 (如 int/embstr/raw) unsigned lru:24; // [8-31
struct redisObject { unsigned type:4; // [0-3 bit] 对象类型 (如 String) unsigned encoding:4; // [4-7 bit] 编码方式 (如 int/embstr/raw) unsigned lru:24; // [8-31 bit] 缓存淘汰数据 int refcount; // [32-63 bit] 引用计数 (4字节) void *ptr; // [64-127 bit] 关键指针 (8字节)};
Redis 的 String 类型,表面上是一个简单的字符串,但在底层却包含三种不同的编码方式:int、embstr 和 raw。之所以这样设计,是因为 String 的数据大小差异较大,Redis 需要根据内容为整数还是文本、长度是短还是长,动态选择最优存储方案,从而在内存占用和性能之间取得平衡。

长期稳定更新的攒劲资源: >>>点此立即查看<<<
这三种编码都统一在 redisObject 这个“外壳”之下,通过 encoding 字段区分当前使用的编码类型。
struct sdshdr8 { uint8_t len; /* 已使用长度 */ uint8_t alloc; /* 总分配空间(不含头和 ) */ unsigned char flags; /* 类型标志(如 sdshdr8, sdshdr16 等) */ char buf[]; /* 实际字节数组 */};
当一个字符串对象保存的是整数值,并且该整数可以用 long 类型(8字节有符号整数)表示时,Redis 会采用 int 编码。此时,Redis 不会额外分配 SDS 空间,而是直接将整数值存储在 redisObject 结构体的 ptr 指针字段中(通过强制类型转换实现)。
当字符串的长度小于等于 44 字节时,Redis 会切换为 embstr 编码。其核心目的是最大限度压榨小对象的性能。
redisObject 和 sdshdr(SDS 头部及数据)在内存中连续存储,通过一次 malloc 申请完成。这意味着分配和释放都只需一次系统调用,内存布局十分紧凑。APPEND)都会强制将其升级为 raw 编码,因为连续内存块无法在原位置安全扩展。redisObject 占 16 字节,sdshdr8 占 3 字节,再加一个结束符 占 1 字节,剩余 44 字节用于数据,整个对象恰好塞进内存分配器的 64 字节 槽位,不浪费任何空间。当字符串长度大于 44 字节,或者对 embstr 执行了修改操作时,Redis 会采用 raw 编码。这种方式适合处理较大的数据。
redisObject 和 sdshdr 分布在两块不连续的内存空间中。ptr 指针指向独立的 SDS 区域。malloc 或 free,开销比 embstr 略大。在 Redis 3.2 之前,List 的实现比较直接:数据量小时使用 ZipList(压缩列表)——连续内存以节省空间;数据量大或字符串较长时,直接使用 LinkedList(双向链表)——通过指针实现灵活增删。但缺点也很明显:每个节点需要两个 8 字节的指针(prev/next),且内存碎片较多。
RedisObject 中的 *ptr 指向 quicklist 对象
typedef struct quicklist { quicklistNode *head; /* 指向头节点 */ quicklistNode *tail; /* 指向尾节点 */ unsigned long count; /* 所有元素总数 */ unsigned long len; /* 节点(车厢)总数 */ int fill : 16; /* 节点填充因子 */ unsigned int compress : 16; /* 压缩深度 */} quicklist;typedef struct quicklistNode { struct quicklistNode *prev; /* 前驱指针 */ struct quicklistNode *next; /* 后继指针 */ unsigned char *zl; /* 指向物理内存中的连续块 (ZipList/Listpack) */ unsigned int sz; /* 连续块占用的总字节数 */ unsigned int count : 16; /* 连续块包含的元素个数 */ // ... 其他标志位} quicklistNode;
Redis 的 Set(集合) 编码设计遵循“从小到大”的进化逻辑。其物理实现主要在 IntSet(整数集合)、Listpack(紧凑列表,Redis 7.2+) 和 Hashtable(哈希表) 之间切换。核心思想可以概括为:如果全是小整数,就用数组排序存储;如果包含字符串,则使用哈希表。
当集合满足以下两个条件时,Redis 优先使用 intset:
set-max-intset-entries(默认 512 个)。intset 是一块绝对连续的内存空间。
int16_t、int32_t 或 int64_t 编码。O(log N)。int16,现在要存 int32)时,会触发整块内存的重新分配和数据迁移。需要注意,这个过程不可逆(不支持降级)——一旦升级到更高位宽,即使后续所有数字都能用低位宽表示,也不会降级,这是为了保持实现简单和性能稳定。这是 Redis 7.2 引入的新物理层。在旧版本中,集合只要出现一个字符串就会直接扩容为 dict,而 listpack 充当了中间的缓冲带,避免过早转型。
set-max-listpack-entries 和 set-max-listpack-value 阈值。O(N)(顺序遍历),但由于数据规模极小,其内存利用率远高于 dict,并且连续内存对 CPU 缓存友好,在小数据量下完全抵消了 O(N) 的劣势。当集合规模超过阈值,或者包含长字符串时,Redis 会使用 dict 作为最终物理载体。
此时 redisObject->ptr 指向一个真实的 dict 结构体实例。
NULL 指针。因为集合只关心成员是否存在,不需要存储值。dict 自身的哈希碰撞处理和 Key 唯一性逻辑实现集合去重。O(1)。支持渐进式 Rehash,在数据量极大时仍能保持稳定的响应速度。对于 Set 来说,redisObject 的包装方式如下:
| 字段 | IntSet 编码 | Hashtable 编码 |
|---|---|---|
| type | OBJ_SET | OBJ_SET |
| encoding | OBJ_ENCODING_INTSET | OBJ_ENCODING_HT |
| ptr 指向 | 一整块连续的 intset 结构 | 一个复杂的 dict 字典结构 |
Redis 的 ZSet(有序集合) 在底层编码上设计最为复杂,因为它必须同时满足两个核心需求:O(1) 成员查分(按成员查分数)和 O(log N) 按分数排序/范围检索。物理实现主要分为两个阶段:listpack 和 dict + zskiplist。
当 ZSet 满足以下两个条件时,Redis 使用 listpack 编码(OBJ_ENCODING_LISTPACK):
zset-max-listpack-entries(默认 128)。zset-max-listpack-value(默认 64 字节)。在 listpack 内部,成员(Member)和分值(Score)存储为两个相邻的 Entry:
[Member1, Score1, Member2, Score2, ...]O(N) 的顺序遍历及内存搬迁。但在小数据量下,这种结构的 CPU 缓存命中率极高,且省去了复杂的指针开销,实现简单。当数据量突破阈值后,redisObject->ptr 会指向一个专门的 zset 结构体。这是一种双重物理结构的组合:
typedef struct zset { dict *dict; /* 成员 -> 分值的哈希表 */ zskiplist *zsl; /* 按分数排序的跳跃表 */} zset;
O(1) 复杂度的 ZSCORE 操作。给定成员名,即可立即查到分数。dict,查找一个成员的分数需要遍历跳表,复杂度为 O(log N),对于高频率的查分操作来说不够快。ZRANGE)和排名计算(ZRANK)。O(log N)。你可能会担心:同一个成员既存在 dict 里,又存在 zskiplist 里,是否浪费内存?
物理真相:dict 的 Key 和 zskiplistNode 的 ele 指向的是同一个物理内存地址(同一个 SDS 对象)。Redis 只是在两个数据结构中各存了一个指针,实际数据只存储一份。这种设计通过增加少量指针开销(每个节点约几十字节),换取了两个维度的极致查询速度——这个取舍非常划算。
| 物理结构 | 逻辑编码 (Encoding) | 核心优势 | 算法复杂度 | 内存特征 |
|---|---|---|---|---|
| listpack | LISTPACK | 极致节省内存 | O(N) (查找/插入) | 连续内存,无碎片 |
| zset (复合) | SKIPLIST | 全能性能 | 查分 O(1),范围 O(log N) | 双重索引,指针较多 |
ZSet 的转换通常是单向不可逆的:一旦数据量超过阈值,listpack 会被拆解,重新装载进一个新的 dict 和 zskiplist 中。原因:从复杂的双重结构回退到连续内存块涉及大规模的内存重分配和 CPU 计算,收益不抵成本。所以一旦升级,就安心使用“重武器”。
Redis 的 Hash(哈希) 结构在底层编码的设计上,逻辑与 ZSet 相似:数据量小时采用紧凑的连续内存,数据量大时进化为散列表。目前的物理实现主要分为 listpack 和 dict 两种。
当 Hash 结构满足以下两个条件时,Redis 使用 listpack 存储(编码名称为 OBJ_ENCODING_LISTPACK):
hash-max-listpack-entries(默认 512 个)。hash-max-listpack-value(默认 64 字节)。在 listpack 的字节流中,Field 和 Value 作为两个相邻的 Entry 存储:
[Field1, Value1, Field2, Value2, ...]listpack 几乎不浪费一个字节。一旦数据量突破阈值,或者某个 Value 太长,Redis 就会将物理结构转换为 dict(编码名称为 OBJ_ENCODING_HT)。
此时 redisObject->ptr 指向一个真实的 dict 结构体:
O(1)。对于大规模 Hash 来说,这是标准配置。侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述