简介
redisObject:即redis对象,redis数据库是以Key-Value形式存在,当新建一个Key-Value对时,至少会创建两个对象,一个用于作为Key对象,一个用于作为Value对象,每个对象都由一个redisObject的结构表示。
数据结构
redisObject数据结构如下(server.h):
#define LRU_BITS 24
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits access time). */
int refcount;
void *ptr;
} robj;
type
表示redis对象所保存的值的类型。
/* The actual Redis Object */
#define OBJ_STRING 0 /* String object. */
#define OBJ_LIST 1 /* List object. */
#define OBJ_SET 2 /* Set object. */
#define OBJ_ZSET 3 /* Sorted set object. */
#define OBJ_HASH 4 /* Hash object. */
encoding
表示redis对象所保存的值对应编码,也就是数据结构。
#define OBJ_ENCODING_RAW 0 /* Raw representation */
#define OBJ_ENCODING_INT 1 /* Encoded as integer */
#define OBJ_ENCODING_HT 2 /* Encoded as hash table */
#define OBJ_ENCODING_ZIPMAP 3 /* Encoded as zipmap */
#define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define OBJ_ENCODING_INTSET 6 /* Encoded as intset */
#define OBJ_ENCODING_SKIPLIST 7 /* Encoded as skiplist */
#define OBJ_ENCODING_EMBSTR 8 /* Embedded sds string encoding */
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */
lru
lru记录了对象空转时长,OBJECT IDLETIME 命令可以打印出给定键的空转时长。
redis> SET key1 "hello world"
OK
# 过段时间执行
redis> OBJECT IDLETIME key1
(integer) 20
OBJECT IDLETIME命令在访问键的值对象时, 不会更新值对象的 lru 属性。
如果服务器打开了 maxmemory 选项, 并且服务器用于回收内存的算法为 volatile-lru 或者 allkeys-lru , 那么当服务器占用的内存数超过了 maxmemory 选项所设置的上限值时, 空转时长较高的那部分键会优先被服务器释放, 从而回收内存。
refcount
Redis中为了更好的优化内存空间,对数字字符串进行了共享内存的操作,并以引用计数方式进行管理(object.c)
// 将对象的引用计数值设置为 0 , 但并不释放对象, 这个函数通常在需要重新设置对象的引用计数值时使用。
robj *resetRefCount(robj *obj) {
obj->refcount = 0;
return obj;
}
// 将对象的引用计数值增一
void incrRefCount(robj *o) {
if (o->refcount < OBJ_FIRST_SPECIAL_REFCOUNT) {
o->refcount++;
} else {
if (o->refcount == OBJ_SHARED_REFCOUNT) {
/* Nothing to do: this refcount is immutable. */
} else if (o->refcount == OBJ_STATIC_REFCOUNT) {
serverPanic("You tried to retain an object allocated in the stack");
}
}
}
// 将对象的引用计数值减一, 当对象的引用计数值等于 0 时, 释放对象。
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;
case OBJ_MODULE: freeModuleObject(o); break;
case OBJ_STREAM: freeStreamObject(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--;
}
}
- 新建一个对象时,它的 refcount = 1 ;
- 对一个对象进行共享时,这个对象的 refcount +1;
- 当使用完一个对象之后,或者取消对共享对象的引用之后,程序将对象的 refcount -1;
- 当对象的 refcount 降至 0 时,释放redisObject 对象及*ptr引用的数据结构。
*ptr
*ptr 是一个指针,指向实际保存值的数据结构,这个数据结构由 type 属性和 encoding 属性决定。
假设redisObject 的 type = OBJ_ZSET, encoding = OBJ_ENCODING_SKIPLIST ,代表当前redisObject是一个基于采用跳跃列表数据结构的有序集合,*ptr指向对应的跳跃列表。
类型检查和命令多态
当执行一个处理数据类型的命令时, Redis 执行以下步骤:
- 根据给定 key ,在数据库字典中查找和它相对应的 redisObject ,如果没找到,就返回 NULL 。
- 判断 redisObject 的 type属性 = 执行命令所需的类型(HSET) ?,如果不相符,返回类型错误。
- 根据 redisObject 的 encoding 属性所指定的编码,选择合适的操作函数来处理底层的数据结构。
- 返回数据结构的操作结果作为命令的返回值。
因此,上述第2步就是类型检查,第3步就是命令多态。
对象共享
除了用于实现引用计数内存回收机制之外, 对象的引用计数属性还带有对象共享的作用。
假设键 key1创建了一个包含整数值 1 的字符串对象作为值对象,这时键key2也要创建一个同样保存了整数值 1 的字符串对象作为值对象,为了让多个键共享同一个值对象需要执行以下两个步骤:
- 将数据库键的值指针指向一个现有的值对象;
- 将被共享的值对象的引用计数增一。
目前来说, Redis 会在初始化服务器时, 创建一万个字符串对象, 这些对象包含了从 0 到 9999 的所有整数值, 当服务器需要用到值为 0到 9999 的字符串对象时, 服务器就会使用这些共享对象, 而不是新创建对象。
创建共享字符串对象的数量可以通过修改 server.h/OBJ_SHARED_INTEGERS常量来修改。
redis> SET key1 1
OK
redis> OBJECT REFCOUNT key1
(integer) 2
该引用计数=2,是因为服务器程序初始化时,内置了该对象了,refcount=1。
另外, 这些共享对象不单单只有字符串键可以使用, 那些在数据结构中嵌套了字符串对象的对象(linkedlist
编码的列表对象、 hashtable
编码的哈希对象、 hashtable
编码的集合对象、以及 zset
编码的有序集合对象)都可以使用这些共享对象。
为什么 Redis 不共享包含字符串的对象?
当服务器考虑将一个共享对象设置为键的值对象时, 程序需要先检查给定的共享对象
和键想创建的目标对象
是否完全相同, 只有在共享对象
和目标对象
完全相同的情况下, 程序才会将共享对象
用作键的值对象, 而一个共享对象
保存的值越复杂, 验证共享对象
和目标对象
是否相同所需的复杂度就会越高, 消耗的 CPU 时间也会越多:
- 如果
共享对象
是保存整数值的字符串对象, 那么验证操作的复杂度为 O(1);- 如果
共享对象
是保存字符串值的字符串对象, 那么验证操作的复杂度为 O(N);- 如果
共享对象
是包含了多个值(或者对象的)对象, 比如列表对象或者哈希对象, 那么验证操作的复杂度将会是 O(N^2) 。
因此, 尽管共享更复杂的对象可以节约更多的内存, 但受到 CPU 时间的限制, Redis 只对包含整数值的字符串对象进行共享。