Redis作为一个基于内存的高性能键值数据库,其核心设计理念之一就是用统一的接口处理多样化的数据类型。在Redis源码中,所有的键值对数据在底层都被封装成一个名为redisObject的结构体。这个结构体不仅是五大数据类型(字符串、列表、哈希、集合、有序集合)的统一载体,还承载了内存管理、编码转换和淘汰策略等关键职责。深入理解redisObject的内部实现,对于掌握Redis的性能优化和内存机制至关重要。

redisObject结构体定义与核心字段解析
在Redis的源码文件server.h中,redisObject的结构定义非常精炼,但每一个字段都经过了精心设计。这个结构体通过位运算和紧凑的内存布局,在尽可能少的字节内存储了尽可能多的元信息。其核心定义包含了type、encoding、lru和refcount四个关键元数据字段,以及一个指向实际数据的指针ptr。
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS; // LRU_BITS通常为24
int refcount;
void *ptr;
} robj;
type字段占用4个比特位,用于标识对象的类型,比如OBJ_STRING、OBJ_LIST、OBJ_HASH等。由于4个比特位最多能表示16种类型,这足以覆盖Redis当前的所有数据类型并留有扩展余地。encoding字段同样占用4个比特位,记录了对象底层所使用的编码方式。这种设计使得同一个数据类型可以有多种底层实现,比如列表类型既可以用ziplist实现,也可以用linkedlist或者quicklist实现,Redis会根据数据量的大小自动选择最优编码。
lru字段占据了24个比特位,这个字段在Redis运行不同淘汰策略时扮演不同角色。当使用LRU淘汰策略时,它记录对象最后一次被访问的时间戳;当使用LFU策略时,它被拆分为两部分,分别记录访问频率的衰减时间和实际访问次数。refcount字段是一个整型,用于引用计数内存管理,当对象被多个地方引用时,refcount会相应增加,当减少到0时对象会被回收。最后的ptr指针指向对象真正的数据存储位置,这个指针的具体指向取决于encoding的值。
type与encoding的映射关系及底层编码转换
Redis最精妙的设计之一就是将数据类型与底层编码解耦。type字段决定了对象对外暴露的接口和行为,而encoding字段决定了数据在内存中的实际存储格式。这种分离使得Redis能够根据数据特征动态选择最优的存储方式,在不改变外部API语义的前提下实现性能和内存占用的最优化。
以字符串对象为例,当字符串内容是整数值且大小不超过long类型的范围时,Redis会使用OBJ_ENCODING_INT编码,直接将整数值存储在ptr指针的位置,避免了额外的内存分配。当字符串较短时,会使用embstr编码,这种编码方式将redisObject结构和SDS(简单动态字符串)分配在一块连续的内存中,提高了缓存命中率。只有当字符串较长或者经过多次修改后,才会转换为raw编码,此时redisObject和SDS分别独立分配内存。
// 字符串对象编码转换逻辑简化版
robj *createStringObject(const char *ptr, size_t len) {
if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)
return createEmbeddedStringObject(ptr, len);
else
return createRawStringObject(ptr, len);
}
// 当字符串被修改时可能触发编码转换
robj *setRange(robj *o, size_t offset, const char *value, size_t valueLen) {
if (o->encoding == OBJ_ENCODING_EMBSTR) {
// embstr是只读的,修改前需要转换为raw
o = createRawStringObject(o->ptr, sdslen(o->ptr));
}
// 执行实际的修改操作
// ...
return o;
}
列表对象的编码转换同样体现了这种动态优化策略。当列表元素较少且每个元素长度较短时,使用ziplist编码可以获得极高的内存利用率,因为ziplist是一段连续的内存块,没有指针开销。但随着元素增多或单个元素变大,ziplist的插入和删除操作会退化为O(n)复杂度,此时Redis会自动将其转换为quicklist编码,这是一种结合了ziplist和linkedlist优点的混合数据结构。这种编码转换机制是Redis在内存占用和操作性能之间寻找平衡点的典型体现。
引用计数机制与对象共享
Redis通过引用计数来实现内存的自动回收。当一个redisObject被创建时,其refcount初始化为1。每当有新的引用指向这个对象时,调用incrRefCount函数使refcount加1;当引用断开时,调用decrRefCount函数使refcount减1。当refcount降为0时,对象所占用的内存会被立即释放。这种机制避免了手动内存管理的复杂性,同时也防止了内存泄漏。
void incrRefCount(robj *o) {
if (o->refcount < OBJ_FIRST_SPECIAL_REFCOUNT) {
o->refcount++;
} else {
if (o->refcount == OBJ_SHARED_REFCOUNT)
/* 共享对象不能修改引用计数 */
;
else if (o->refcount == OBJ_STATIC_REFCOUNT)
/* 静态对象不能修改引用计数 */
;
}
}
void decrRefCount(robj *o) {
if (o->refcount == 1) {
switch (o->type) {
case OBJ_STRING: freeStringObject(o); break;
case OBJ_LIST: freeListObject(o); break;
case OBJ_SET: freeSetObject(o); break;
case OBJ_ZSET: freeZsetObject(o); break;
case OBJ_HASH: freeHashObject(o); break;
default: serverPanic("Unknown object type"); break;
}
zfree(o);
} else {
if (o->refcount <= 0) serverPanic("decrRefCount against refcount <= 0");
if (o->refcount != OBJ_SHARED_REFCOUNT) o->refcount--;
}
}
基于引用计数机制,Redis实现了对象共享功能。在Redis启动时,会预先创建一定数量的共享整数对象,这些对象的refcount被设置为特殊的OBJ_SHARED_REFCOUNT值,表示这是一个共享对象,其引用计数不会被修改也不会被回收。当客户端存储一个小的整数值时,Redis不会创建新的redisObject,而是直接返回一个指向共享对象的指针。这种设计在存储大量重复小整数的场景下能显著节省内存。不过出于性能考虑,Redis目前只对整数值启用了对象共享,字符串对象由于验证相等性成本较高而没有启用。
LRU与LFU时钟机制在对象中的实现
redisObject中的lru字段是Redis实现内存淘汰策略的核心组件。这个24位的字段在不同的淘汰策略下有着完全不同的数据解释方式。当配置为LRU策略时,这24位存储的是对象最后一次被访问时的Unix时间戳的低24位。由于24位最大能表示的值为16777215秒(约194天),Redis通过取时钟的低24位来模拟LRU行为,虽然存在理论上的时钟回绕问题,但在实际运行中影响极小。
当配置为LFU策略时,这24位被拆分为两部分:高16位存储上次访问衰减时间,低8位存储访问次数的对数计数器。LFU算法的核心思想是频繁访问的数据应该被保留,而长期未访问的数据应该被淘汰。由于8位最多只能记录255次访问,Redis采用了一种对数概率算法来模拟访问频率,使得计数器的增长速度随着访问次数增加而减缓,从而避免了计数器溢出的问题。
// LFU计数器更新逻辑
uint8_t LFULogIncr(uint32_t counter) {
if (counter == 255) return 255;
double r = (double)rand()/RAND_MAX;
double baseval = counter - LFU_INIT_VAL;
if (baseval < 0) baseval = 0;
double p = 1.0/(baseval*lfu_log_factor+1);
if (r < p) counter++;
return counter;
}
// LFU访问衰减逻辑
unsigned long LFUDecrAndReturn(robj *o) {
unsigned long ldt = LFUGetTimeInMinutes();
unsigned long counter = o->lru & 255;
unsigned long decay = (ldt - o->lru >> 8) * lfu_decay_time;
if (decay > counter) counter = 0;
else counter -= decay;
return counter;
}
衰减机制是LFU算法能够适应数据访问模式变化的关键。如果只有递增没有递减,那么曾经频繁访问但后来不再访问的数据将永远不会被淘汰。Redis通过记录上次访问时间,在每次访问时计算时间差并按比例衰减访问次数,使得历史热点数据在长期不被访问后能够自然冷却,从而为新热点数据腾出空间。这种设计使得Redis的内存淘汰策略既考虑了访问频率,又考虑了时间局部性,在实际生产环境中表现出了优秀的命中率。
redisObjectRedis数据结构Redis源码分析修改时间:2026-08-24 04:42:55