导读:本期聚焦于本地能跑创作的《Redis是如何通过redisObject对象系统管理不同数据类型的?源码深度解析》,敬请观看详情。Redis底层并没有为每种数据类型单独设计一套完全独立的内存管理机制,而是通过一个名为redisObject的统一结构体来封装和表示所有的键值对数据。这个结构体不仅记录了数据的实际类型,还包含了编码方式、内存回收策略以及引用计数等核心信息。理解redisObject的内部构造,是掌握Redis高效内存利用和对象共享机制的关键所在。本文将从源码层面拆解redisObject的结构定义,分析type与encoding字段如何配合工作,深入探讨LRU与LFU算法在对象元数据中的实现细节,并揭示引用计数机制如何支撑Redis的对象共享与自动内存回收。

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

Redis是如何通过redisObject对象系统管理不同数据类型的?源码深度解析

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_STRINGOBJ_LISTOBJ_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编码,这是一种结合了ziplistlinkedlist优点的混合数据结构。这种编码转换机制是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

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。