各位观众,晚上好!欢迎来到今天的“Redis探秘之对象戏法”讲座。今天咱们要聊的是Redis世界里的一个关键角色——robj
,也就是Redis Object。别看它名字平平无奇,它可是Redis能玩转各种数据类型的幕后英雄,同时也是内存管理大师。
咱们先来想个问题:Redis为啥能存字符串、列表、集合、哈希表等等各种类型的数据?难道它内部针对每种类型都实现一套存储和操作逻辑?那也太累了吧!答案当然是No!Redis聪明的地方在于,它使用robj
这个统一的对象结构,把各种类型的数据都包装起来,这样就可以用一套通用的机制来管理它们。
1. robj
:万物皆对象
robj
的定义在redis.h
文件中,咱们先来看看它的庐山真面目(简化版):
typedef struct redisObject {
unsigned type:4; // 对象类型
unsigned encoding:4; // 对象编码
unsigned lru:REDIS_LRU_BITS; // LRU 时间(用于内存淘汰)
int refcount; // 引用计数
void *ptr; // 指向底层数据结构的指针
} robj;
-
type
(对象类型):这个字段指明了robj
所代表的数据类型,比如REDIS_STRING
(字符串)、REDIS_LIST
(列表)、REDIS_SET
(集合)、REDIS_HASH
(哈希表)、REDIS_ZSET
(有序集合)。Redis根据这个类型来决定如何处理这个对象。 -
encoding
(对象编码):同一个类型的数据,在底层可能采用不同的编码方式来存储,以提高效率。比如,字符串可以用RAW
(简单动态字符串,SDS)编码,也可以用INT
(整数)编码。列表可以用ziplist
(压缩列表)编码,也可以用linkedlist
(双向链表)编码。encoding
字段就是用来标识底层编码方式的。 -
lru
(LRU时间):这个字段记录了对象最后一次被访问的时间,用于LRU(Least Recently Used)算法,在内存不足时淘汰最近最少使用的对象。 -
refcount
(引用计数):这个字段是robj
实现内存管理的关键。它记录了有多少地方引用了这个对象。当refcount
为0时,表示这个对象不再被使用,可以被释放。 -
ptr
(指针):这个指针指向实际存储数据的底层数据结构。根据type
和encoding
的不同,ptr
可能指向SDS、整数、压缩列表、跳跃表等等。
来个表格总结一下:
字段 | 含义 | 取值示例 |
---|---|---|
type |
对象类型 | REDIS_STRING , REDIS_LIST , REDIS_SET , REDIS_HASH , REDIS_ZSET |
encoding |
对象编码 (底层数据结构) | REDIS_ENCODING_RAW , REDIS_ENCODING_INT , REDIS_ENCODING_ZIPLIST , REDIS_ENCODING_LINKEDLIST , REDIS_ENCODING_HT , REDIS_ENCODING_SKIPLIST |
lru |
LRU时间 (用于内存淘汰) | 时间戳 |
refcount |
引用计数 | 整数 |
ptr |
指向底层数据结构的指针,根据type 和encoding 不同指向不同结构 |
sds , long , ziplist , list , dict , zskiplist |
2. 对象类型与编码:千变万化
咱们来详细看看Redis支持的几种对象类型,以及它们可以使用的编码方式。
2.1 字符串对象 (String)
字符串对象是Redis中最基本的数据类型。它可以存储文本数据,也可以存储二进制数据。
type
:REDIS_STRING
encoding
:REDIS_ENCODING_INT
: 如果字符串对象的内容可以表示为一个整数,Redis会使用INT
编码来存储,将整数直接保存在robj
的ptr
字段中。这种方式非常节省内存。REDIS_ENCODING_RAW
: 如果字符串对象的内容无法表示为一个整数,Redis会使用RAW
编码来存储,将字符串保存在一个SDS(Simple Dynamic String,简单动态字符串)中,robj
的ptr
字段指向这个SDS。REDIS_ENCODING_EMBSTR
: 这是一种优化的RAW
编码。当字符串长度小于等于REDIS_ENCODING_EMBSTR_SIZE_LIMIT
(通常是39字节)时,Redis会使用EMBSTR
编码,将robj
对象头和SDS对象连续地存储在同一块内存中,这样可以减少内存分配的次数,提高效率。
编码转换:
- 如果对一个
INT
编码的字符串对象执行了某些命令,使得它不再能表示为一个整数,那么它会被转换为RAW
编码。 - 当创建一个新的字符串对象,且字符串长度小于等于
REDIS_ENCODING_EMBSTR_SIZE_LIMIT
时,会优先使用EMBSTR
编码。
举个栗子:
// 创建一个整数类型的字符串对象
robj *createStringObjectFromLongLong(long long value) {
robj *o = zmalloc(sizeof(robj));
o->type = REDIS_STRING;
o->encoding = REDIS_ENCODING_INT;
o->ptr = (void*)((long)value); // 直接将整数值存储在 ptr 中
o->refcount = 1;
return o;
}
// 创建一个 SDS 类型的字符串对象
robj *createStringObject(const char *s, size_t len) {
robj *o = zmalloc(sizeof(robj));
o->type = REDIS_STRING;
if (len <= REDIS_ENCODING_EMBSTR_SIZE_LIMIT) {
// 使用 EMBSTR 编码
size_t alloclen = sizeof(robj) + sdslen(sdsnewlen(s,len));
o = zmalloc(alloclen);
o->type = REDIS_STRING;
o->encoding = REDIS_ENCODING_EMBSTR;
o->ptr = sdscatlen(sdsnewlen(s,len),s,len);
} else {
// 使用 RAW 编码
o->encoding = REDIS_ENCODING_RAW;
o->ptr = sdsnewlen(s,len);
}
o->refcount = 1;
return o;
}
2.2 列表对象 (List)
列表对象用于存储一个有序的字符串列表。
type
:REDIS_LIST
encoding
:REDIS_ENCODING_ZIPLIST
: 当列表对象同时满足以下两个条件时,Redis会使用ZIPLIST
编码:- 列表对象保存的所有字符串元素的长度都小于
list-max-ziplist-value
配置项的值(默认64字节)。 - 列表对象包含的元素数量小于
list-max-ziplist-entries
配置项的值(默认512个)。
- 列表对象保存的所有字符串元素的长度都小于
REDIS_ENCODING_LINKEDLIST
: 当列表对象不满足ZIPLIST
编码的条件时,Redis会使用LINKEDLIST
编码,底层是一个双向链表。
编码转换:
- 当列表对象中插入了长度超过
list-max-ziplist-value
的字符串元素,或者列表对象的元素数量超过list-max-ziplist-entries
时,ZIPLIST
编码会被转换为LINKEDLIST
编码。这种转换是不可逆的。
为啥要有两种编码?
ZIPLIST
(压缩列表)是一种非常紧凑的数据结构,它可以节省大量的内存空间。但是,ZIPLIST
的插入和删除操作的效率相对较低,因为可能需要移动大量的元素。LINKEDLIST
(双向链表)的插入和删除操作的效率很高,但是它会消耗更多的内存空间,因为每个节点都需要额外的指针来指向前一个和后一个节点。
举个栗子:
// 向列表中添加一个元素 (简化的例子)
void listAddNodeTail(list *list, void *value) {
listNode *node = zmalloc(sizeof(listNode));
node->value = value;
if (list->len == 0) {
list->head = list->tail = node;
node->prev = node->next = NULL;
} else {
node->prev = list->tail;
node->next = NULL;
list->tail->next = node;
list->tail = node;
}
list->len++;
}
2.3 集合对象 (Set)
集合对象用于存储一个无序的、唯一的字符串集合。
type
:REDIS_SET
encoding
:REDIS_ENCODING_INTSET
: 当集合对象同时满足以下两个条件时,Redis会使用INTSET
编码:- 集合对象保存的所有元素都是整数值。
- 集合对象包含的元素数量不超过
set-max-intset-entries
配置项的值(默认512个)。
REDIS_ENCODING_HT
: 当集合对象不满足INTSET
编码的条件时,Redis会使用HT
编码,底层是一个哈希表,其中键是集合元素,值都是NULL
。
编码转换:
- 当集合对象中插入了非整数值的元素,或者集合对象的元素数量超过
set-max-intset-entries
时,INTSET
编码会被转换为HT
编码。这种转换也是不可逆的。
INTSET 编码的优势:
INTSET
(整数集合)是一种专门用于存储整数值集合的数据结构,它可以根据整数值的范围自动选择合适的存储类型(int16_t
、int32_t
或int64_t
),从而节省内存空间。
举个栗子:
// 向整数集合中添加一个整数 (简化的例子)
int intsetAdd(intset *is, int64_t value, uint8_t *success) {
uint8_t valenc = _intsetValueEncoding(value); // 获取值的编码类型
uint32_t pos;
if (!intsetSearch(is,value,&pos)) { // 检查值是否已经存在
is = intsetResize(is,_intsetLen(is)+1); // 调整整数集合的大小
if (valenc > is->encoding) { // 如果新值的编码类型大于当前编码类型,需要升级
is = intsetUpgradeAndAdd(is,value);
*success = 1;
return is;
} else {
memmove(is->contents+((pos+1)*is->encoding),
is->contents+(pos*is->encoding),
((_intsetLen(is)-pos)*is->encoding));
_intsetSet(is,pos,value);
is->length++;
*success = 1;
return is;
}
}
*success = 0;
return is;
}
2.4 哈希对象 (Hash)
哈希对象用于存储一个键值对集合,其中键和值都可以是字符串。
type
:REDIS_HASH
encoding
:REDIS_ENCODING_ZIPLIST
: 当哈希对象同时满足以下两个条件时,Redis会使用ZIPLIST
编码:- 哈希对象保存的所有键和值的字符串长度都小于
hash-max-ziplist-value
配置项的值(默认64字节)。 - 哈希对象包含的键值对数量小于
hash-max-ziplist-entries
配置项的值(默认512个)。
- 哈希对象保存的所有键和值的字符串长度都小于
REDIS_ENCODING_HT
: 当哈希对象不满足ZIPLIST
编码的条件时,Redis会使用HT
编码,底层是一个哈希表。
编码转换:
- 当哈希对象中插入了键或值的字符串长度超过
hash-max-ziplist-value
,或者哈希对象的键值对数量超过hash-max-ziplist-entries
时,ZIPLIST
编码会被转换为HT
编码。这种转换也是不可逆的。
举个栗子:
// 哈希表添加键值对 (简化的例子)
int dictAdd(dict *d, void *key, void *val) {
// ... 省略一些检查和调整大小的代码 ...
dictEntry *entry = zmalloc(sizeof(dictEntry));
entry->key = key;
dictSetVal(d, entry, val);
entry->next = h->table[index];
h->table[index] = entry;
d->used++;
return DICT_OK;
}
2.5 有序集合对象 (ZSet)
有序集合对象用于存储一个有序的、唯一的字符串集合,每个元素都关联一个double类型的分数(score)。Redis根据分数对元素进行排序。
type
:REDIS_ZSET
encoding
:REDIS_ENCODING_ZIPLIST
: 当有序集合对象同时满足以下两个条件时,Redis会使用ZIPLIST
编码:- 有序集合对象保存的所有元素字符串长度都小于
zset-max-ziplist-value
配置项的值(默认64字节)。 - 有序集合对象包含的元素数量小于
zset-max-ziplist-entries
配置项的值(默认128个)。
- 有序集合对象保存的所有元素字符串长度都小于
REDIS_ENCODING_SKIPLIST
: 当有序集合对象不满足ZIPLIST
编码的条件时,Redis会使用SKIPLIST
编码,底层是一个跳跃表和一个哈希表的组合。跳跃表用于实现有序性,哈希表用于实现快速查找元素。
编码转换:
- 当有序集合对象中插入了元素字符串长度超过
zset-max-ziplist-value
,或者有序集合对象的元素数量超过zset-max-ziplist-entries
时,ZIPLIST
编码会被转换为SKIPLIST
编码。这种转换也是不可逆的。
跳跃表(Skip List)的优势:
- 跳跃表是一种概率型数据结构,它可以在O(log N)的时间复杂度内完成查找、插入和删除操作,性能接近于平衡树,但实现起来比平衡树简单得多。
举个栗子:
// 跳跃表插入一个元素 (简化的例子)
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
// ... 省略查找插入位置的代码 ...
zskiplistNode *x = zmalloc(sizeof(zskiplistNode) + sizeof(struct zskiplistLevel)*(level+1));
x->score = score;
x->ele = ele;
// ... 省略更新跳跃表指针的代码 ...
zsl->length++;
return x;
}
3. 引用计数:内存管理小能手
robj
的refcount
字段是实现内存管理的关键。Redis使用引用计数来跟踪对象的使用情况。
- 创建对象时:
refcount
初始化为 1。 - 对象被引用时:
refcount
加 1。 - 对象不再被引用时:
refcount
减 1。 refcount
为 0 时: 对象被释放,内存被回收。
引用计数的好处:
- 简单高效: 引用计数实现简单,开销小,可以快速地回收不再使用的对象。
- 避免内存泄漏: 只要正确地管理对象的引用计数,就可以避免内存泄漏。
举个栗子:
// 增加对象的引用计数
void incrRefCount(robj *o) {
o->refcount++;
}
// 减少对象的引用计数
void decrRefCount(robj *o) {
if (o->refcount <= 0) return; // 防止重复释放
if (o->refcount == 1) {
// 释放对象
switch(o->type) {
case REDIS_STRING: freeStringObject(o); break;
case REDIS_LIST: freeListObject(o); break;
case REDIS_SET: freeSetObject(o); break;
case REDIS_HASH: freeHashObject(o); break;
case REDIS_ZSET: freeZsetObject(o); break;
default: break;
}
zfree(o);
} else {
o->refcount--;
}
}
// 释放字符串对象
void freeStringObject(robj *o) {
if (o->encoding == REDIS_ENCODING_RAW || o->encoding == REDIS_ENCODING_EMBSTR) {
sdsfree(o->ptr);
}
}
// 释放列表对象
void freeListObject(robj *o) {
listRelease((list*)o->ptr);
}
// 释放集合对象
void freeSetObject(robj *o) {
if (o->encoding == REDIS_ENCODING_INTSET)
intsetFree((intset*)o->ptr);
else
dictRelease((dict*)o->ptr);
}
// 释放哈希对象
void freeHashObject(robj *o) {
dictRelease((dict*)o->ptr);
}
// 释放有序集合对象
void freeZsetObject(robj *o) {
zset *zs = o->ptr;
dictRelease(zs->dict);
zslFree(zs->zsl);
zfree(zs);
}
共享对象:
为了进一步节省内存,Redis还使用了一种叫做“共享对象”的机制。Redis会预先创建一些常用的对象,比如0到9999的整数,以及一些常用的字符串,并将它们的refcount
设置为一个很大的值。当需要使用这些对象时,可以直接引用它们,而不需要创建新的对象。
注意:
- 只有不变对象(immutable object)才能被共享,因为共享对象可能会被多个地方引用,如果允许修改共享对象,可能会导致数据不一致。
4. 总结:robj
的意义
robj
在 Redis 中扮演着至关重要的角色,它:
- 实现了统一的对象模型: 使得 Redis 可以用一套通用的机制来管理各种类型的数据。
- 支持多种编码方式: 使得 Redis 可以根据数据的特点选择合适的编码方式,以提高效率和节省内存。
- 使用引用计数进行内存管理: 使得 Redis 可以自动地回收不再使用的对象,避免内存泄漏。
- 通过共享对象节省内存: 进一步优化内存使用。
总而言之,robj
是 Redis 高效运行的基础,它体现了 Redis 在数据结构和内存管理方面的精妙设计。 理解 robj
的原理,有助于我们更好地理解 Redis 的内部机制,从而更好地使用 Redis。
好了,今天的“Redis探秘之对象戏法”就到这里了。感谢各位的观看,希望大家有所收获!下次再见!