Redis 的对象共享机制和 refcount 内存管理

Redis 作为一款高性能的内存数据库,其内存管理机制一直是面试中的高频考点。很多开发者在使用 Redis 时,只关注它的命令和数据结构,却忽略了底层的内存管理细节。其中,对象共享机制refcount 引用计数是理解 Redis 内存优化的关键。本文将从底层实现出发,带你彻底搞懂这两个机制。

为什么 Redis 需要对象共享?

Redis 是一个内存数据库,内存资源非常宝贵。如果每个对象都独立分配内存,那么当大量键存储相同的值时,内存浪费会非常严重。举个简单的例子:假设有 10000 个键的值都是整数 100,如果每个值都单独分配一个对象,就需要 10000 个对象;而如果让它们共享同一个对象,只需要 1 个对象加 10000 个指针即可。

这就是对象共享机制的核心思想:让多个键共享同一个值对象,从而节省内存

Redis 对象的底层结构

在 Redis 中,每个对象都由 redisObject 结构表示:

typedef struct redisObject {
    unsigned type:4;       // 对象类型:string、list、hash、set、zset
    unsigned encoding:4;   // 编码方式:int、embstr、raw、ziplist 等
    unsigned lru:LRU_BITS; // LRU 时间戳或 LFU 计数
    int refcount;          // 引用计数
    void *ptr;             // 指向底层数据结构的指针
} robj;

其中 refcount 字段就是引用计数,它记录了当前对象被多少个键或其他对象引用。ptr 指针指向真正的数据。

refcount 引用计数的三种状态

refcount 的值在不同场景下有不同的含义:

  • refcount > 1:对象被多个键共享,属于共享对象。
  • refcount == 1:对象只被一个键引用,属于独占对象。
  • refcount == 0:对象不再被任何键引用,可以被回收。

Redis 通过 incrRefCountdecrRefCount 两个函数来管理引用计数:

void incrRefCount(robj *o) {
    if (o->refcount < OBJ_FIRST_SHARED) {
        o->refcount++;
    }
}

void decrRefCount(robj *o) {
    if (o->refcount == 1) {
        // 引用计数为 1 时,真正释放对象
        freeObj(o);
    } else {
        o->refcount--;
    }
}

注意 OBJ_FIRST_SHARED 这个常量,它的值是 INT_MAX - 1000,用于标记共享对象的特殊状态,避免共享对象的引用计数被无限递增导致溢出。

共享对象的创建:共享整数池

Redis 在启动时,会预先创建一批共享对象,最典型的就是 0 到 9999 的整数。这些整数对象被放在一个全局数组中:

struct sharedObjectsStruct {
    robj *integers[OBJ_SHARED_INTEGERS]; // 默认 10000 个
    robj *bulkStrings[OBJ_SHARED_BULKSIZE]; // 共享字符串
    // ...
};

当 Redis 需要创建一个值为 0~9999 的字符串对象时,它不会新建对象,而是直接返回共享池中的对象,并递增其引用计数。这样,无论多少个键存储这些整数值,都只会占用一份内存。

共享对象的条件

并非所有对象都能被共享,Redis 对共享对象有严格的条件限制:

  1. 值必须是整数:只有整数值才能被共享,因为整数的比较和存储成本低。
  2. 值在 0~9999 范围内:这个范围由 OBJ_SHARED_INTEGERS 决定,可以通过配置调整。
  3. 编码必须是 int 编码:如果字符串对象被编码为 embstr 或 raw,则不会共享。

为什么只共享整数?因为共享对象需要判断两个对象是否相等。对于整数,直接比较 ptr 指针即可;对于字符串,需要逐字符比较,成本太高,反而得不偿失。

共享对象的实际应用场景

场景一:大量键存储相同整数

假设你有一个计数器系统,很多键的值都是 10。如果没有共享机制,每个键都会创建一个新对象;有了共享机制,所有值为 1 的键都指向同一个共享对象,内存占用大幅降低。

场景二:LRU 和 LFU 中的共享

Redis 的 LRU 和 LFU 算法中,也会用到共享对象来标记一些特殊状态,减少对象创建开销。

场景三:集合中的整数元素

当集合(Set)中存储的是整数时,如果使用 intset 编码,元素直接存储在数组中,不涉及对象共享;但如果使用 hashtable 编码,整数元素会作为字符串对象存储,此时共享机制就能发挥作用。

共享对象的限制与注意事项

虽然对象共享能节省内存,但它也有一些限制:

  1. 只共享整数:字符串、列表、哈希等对象不会被共享。
  2. 共享池大小有限:默认只有 0~9999,超出范围的整数不会共享。
  3. 引用计数溢出保护:共享对象的 refcount 不会无限递增,达到 OBJ_FIRST_SHARED 后就不再增加。
  4. 修改共享对象会触发写时复制:如果尝试修改一个共享对象,Redis 会先创建一个新对象,再修改新对象,避免影响其他引用者。

写时复制(Copy-On-Write)示例

// 假设 key1 和 key2 都共享整数对象 100
// 现在要修改 key1 的值为 200
robj *new = createStringObjectFromLongLong(200);
dbOverwrite(db, key1, new); // key1 指向新对象,key2 仍指向共享对象 100

面试常见问题

Q1:Redis 的共享对象机制是什么?

Redis 在启动时创建 0~9999 的共享整数对象,当多个键存储相同的整数值时,它们共享同一个对象,通过引用计数管理生命周期,从而节省内存。

Q2:refcount 为 0 时会发生什么?

refcount 为 0 表示对象不再被任何键引用,Redis 会调用 freeObj 释放对象占用的内存。

Q3:为什么 Redis 只共享整数对象?

因为共享对象需要判断相等性,整数比较成本低(直接比较指针),而字符串比较需要逐字符对比,成本高,共享带来的收益不足以抵消比较开销。

Q4:共享对象的 refcount 会无限增长吗?

不会。Redis 设置了 OBJ_FIRST_SHARED 阈值,当 refcount 达到该值后不再递增,避免整数溢出。

Q5:如何查看一个对象的引用计数?

可以使用 OBJECT REFCOUNT key 命令查看指定键对应对象的引用计数。

总结

Redis 的对象共享机制和 refcount 引用计数是内存优化的核心手段。通过预创建共享整数池,Redis 让大量相同整数值的键共享同一份内存;通过引用计数,Redis 精确管理对象的生命周期,避免内存泄漏和悬空指针。理解这两个机制,不仅能帮助你在面试中脱颖而出,还能在实际开发中更好地优化 Redis 内存使用。

记住几个关键点:共享对象只针对 0~9999 的整数refcount 为 0 时释放对象修改共享对象会触发写时复制。掌握这些,你就真正理解了 Redis 的内存管理精髓。

未经允许不得转载:任鹏个人博客 » Redis 的对象共享机制和 refcount 内存管理

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏