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 通过 incrRefCount 和 decrRefCount 两个函数来管理引用计数:
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 对共享对象有严格的条件限制:
- 值必须是整数:只有整数值才能被共享,因为整数的比较和存储成本低。
- 值在 0~9999 范围内:这个范围由
OBJ_SHARED_INTEGERS决定,可以通过配置调整。 - 编码必须是 int 编码:如果字符串对象被编码为 embstr 或 raw,则不会共享。
为什么只共享整数?因为共享对象需要判断两个对象是否相等。对于整数,直接比较 ptr 指针即可;对于字符串,需要逐字符比较,成本太高,反而得不偿失。
共享对象的实际应用场景
场景一:大量键存储相同整数
假设你有一个计数器系统,很多键的值都是 1 或 0。如果没有共享机制,每个键都会创建一个新对象;有了共享机制,所有值为 1 的键都指向同一个共享对象,内存占用大幅降低。
场景二:LRU 和 LFU 中的共享
Redis 的 LRU 和 LFU 算法中,也会用到共享对象来标记一些特殊状态,减少对象创建开销。
场景三:集合中的整数元素
当集合(Set)中存储的是整数时,如果使用 intset 编码,元素直接存储在数组中,不涉及对象共享;但如果使用 hashtable 编码,整数元素会作为字符串对象存储,此时共享机制就能发挥作用。
共享对象的限制与注意事项
虽然对象共享能节省内存,但它也有一些限制:
- 只共享整数:字符串、列表、哈希等对象不会被共享。
- 共享池大小有限:默认只有 0~9999,超出范围的整数不会共享。
- 引用计数溢出保护:共享对象的 refcount 不会无限递增,达到
OBJ_FIRST_SHARED后就不再增加。 - 修改共享对象会触发写时复制:如果尝试修改一个共享对象,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 内存管理

