好的,没问题!
各位观众老爷们,今天咱们来聊聊Redis的“对象系统”,听起来是不是有点高大上?其实一点都不难,说白了,就是Redis怎么管理它里面的数据,以及怎么省着点用内存。
Redis作为一个内存数据库,寸土寸金,每一块内存都要精打细算。所以,它搞了一套“对象系统”,就像一个精明的管家,把各种数据都打理得井井有条。
对象系统是什么?
简单来说,Redis里存储的任何东西,都不是直接存的,而是封装成了一个redisObject
结构体。这个结构体就像一个“百宝箱”,里面装着真实的数据,还记录着数据的类型、编码方式,以及一些其他的元数据。
typedef struct redisObject {
unsigned type:4; // 对象类型 (5种)
unsigned encoding:4; // 对象编码 (8种)
unsigned lru:LRU_BITS; // LRU时间 (用于淘汰)
int refcount; // 引用计数
void *ptr; // 指向底层数据结构的指针
} robj;
看到没?这个redisObject
就是核心。咱们一点点来解剖它:
-
type
(对象类型): 告诉我们这个“百宝箱”里装的是什么类型的宝贝,比如字符串、列表、哈希、集合、有序集合。 -
encoding
(对象编码): 告诉我们这个宝贝是怎么存放的,也就是底层的数据结构。同一个类型的宝贝,可以有不同的存放方式,这样可以根据实际情况选择最省内存、最快的方案。 -
lru
(LRU时间): 这个是用来做内存淘汰的。当Redis内存不够用的时候,会把最久没用过的宝贝给“扔掉”,腾出空间给新来的。这个lru
记录的就是这个宝贝上次被使用的时间,帮助Redis判断谁该“滚蛋”。 -
refcount
(引用计数): 这个很重要,用来做内存共享的。如果多个地方都用到了同一个宝贝,Redis不会傻乎乎地复制多份,而是让它们都指向同一个宝贝,然后把refcount
加1。当一个地方不用这个宝贝了,就把refcount
减1。当refcount
变成0的时候,说明这个宝贝彻底没用了,Redis就可以把它回收了。 -
ptr
(指针): 指向实际存储数据的内存地址。
对象类型(Type):五大金刚
Redis支持五种基本数据类型,也就是type
字段的取值:
类型常量 | 类型名称 | 描述 |
---|---|---|
REDIS_STRING |
字符串 (string) | 可以是字符串、整数、浮点数,但最终都以字符串形式存储。 |
REDIS_LIST |
列表 (list) | 有序的字符串列表。可以从头部或尾部添加、删除元素。 |
REDIS_HASH |
哈希 (hash) | 键值对的集合,类似于字典。 |
REDIS_SET |
集合 (set) | 无序、唯一的字符串集合。 |
REDIS_ZSET |
有序集合 (zset) | 有序的字符串集合,每个元素都有一个分数(score),用于排序。 |
对象编码(Encoding):八面玲珑
每种数据类型,根据数据的大小、复杂度等因素,可以选择不同的编码方式,也就是encoding
字段的取值。不同的编码方式,底层使用的数据结构也不同。
1. 字符串(string):
REDIS_ENCODING_INT
: 如果字符串可以被解释成整数,就用这种编码。直接把整数值存在ptr
里,省空间。REDIS_ENCODING_RAW
: 如果字符串比较长,或者不能被解释成整数,就用这种编码。底层用简单动态字符串(SDS)来存储。REDIS_ENCODING_EMBSTR
: 如果字符串比较短,就用这种编码。把redisObject
和SDS放在一块连续的内存里,减少内存碎片。
代码示例:
// 创建一个整数类型的字符串对象
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;
o->encoding = REDIS_ENCODING_RAW;
o->ptr = sdsnewlen(s, len); // 创建一个SDS
o->refcount = 1;
return o;
}
2. 列表(list):
REDIS_ENCODING_ZIPLIST
: 如果列表的元素比较少,而且每个元素都比较小,就用这种编码。底层用压缩列表(ziplist)来存储。ziplist是一种非常紧凑的数据结构,可以节省大量的内存。REDIS_ENCODING_LINKEDLIST
: 如果列表的元素比较多,或者某个元素比较大,就用这种编码。底层用双向链表(linkedlist)来存储。双向链表插入、删除元素比较快,但是占用空间比较大。REDIS_ENCODING_QUICKLIST
: Redis 3.2 引入的新编码,结合了ziplist和linkedlist的优点。它是一个ziplist的链表,每个ziplist可以存储多个元素。这样既可以节省内存,又可以提高性能。
3. 哈希(hash):
REDIS_ENCODING_ZIPLIST
: 如果哈希的键值对比较少,而且每个键值对都比较小,就用这种编码。底层用压缩列表(ziplist)来存储。REDIS_ENCODING_HT
: 如果哈希的键值对比较多,或者某个键值对比较大,就用这种编码。底层用字典(dict)来存储。字典是一种哈希表,可以快速地查找键值对。
4. 集合(set):
REDIS_ENCODING_INTSET
: 如果集合的所有元素都是整数,就用这种编码。底层用整数集合(intset)来存储。intset是一种有序、唯一的整数集合,可以节省内存。REDIS_ENCODING_HT
: 如果集合的元素不是整数,或者整数的范围超出了intset的范围,就用这种编码。底层用字典(dict)来存储。字典的键存储集合的元素,值都设置为NULL。
5. 有序集合(zset):
REDIS_ENCODING_ZIPLIST
: 如果有序集合的元素比较少,而且每个元素都比较小,就用这种编码。底层用压缩列表(ziplist)来存储。ziplist的元素按照score从小到大排序。REDIS_ENCODING_SKIPLIST
: 如果有序集合的元素比较多,或者某个元素比较大,就用这种编码。底层用跳跃表(skiplist)和字典(dict)来存储。skiplist用于按照score排序,dict用于根据元素查找score。
总结一下各种类型的encoding:
类型 | 编码方式 | 底层数据结构 | 适用场景 |
---|---|---|---|
string | REDIS_ENCODING_INT | int | 可以被解释成整数的字符串 |
string | REDIS_ENCODING_RAW | SDS | 较长的字符串,或者不能被解释成整数的字符串 |
string | REDIS_ENCODING_EMBSTR | SDS (连续内存) | 较短的字符串 |
list | REDIS_ENCODING_ZIPLIST | ziplist | 元素少且小的列表 |
list | REDIS_ENCODING_LINKEDLIST | linkedlist | 元素多或元素大的列表 |
list | REDIS_ENCODING_QUICKLIST | ziplist链表 | 结合了ziplist和linkedlist的优点 |
hash | REDIS_ENCODING_ZIPLIST | ziplist | 键值对少且小的哈希 |
hash | REDIS_ENCODING_HT | dict | 键值对多或键值对大的哈希 |
set | REDIS_ENCODING_INTSET | intset | 所有元素都是整数的集合 |
set | REDIS_ENCODING_HT | dict | 元素不是整数,或者整数范围超出intset范围的集合 |
zset | REDIS_ENCODING_ZIPLIST | ziplist | 元素少且小的有序集合 |
zset | REDIS_ENCODING_SKIPLIST | skiplist + dict | 元素多或元素大的有序集合 |
编码转换:见机行事
Redis并不是一开始就确定一个对象的编码方式,而是会根据数据的变化,动态地进行编码转换。
比如,一个字符串一开始是用REDIS_ENCODING_INT
编码的,但如果后来往里面追加了一个非数字字符,Redis就会把它转换成REDIS_ENCODING_RAW
编码。
类似地,如果一个列表一开始是用REDIS_ENCODING_ZIPLIST
编码的,但如果后来元素变得太多,或者某个元素变得太大,Redis就会把它转换成REDIS_ENCODING_LINKEDLIST
编码。
这种动态编码转换的机制,使得Redis可以根据实际情况,选择最合适的编码方式,从而提高性能和节省内存。
LRU 计数器:谁最不重要?
前面说了,lru
字段记录的是对象上次被访问的时间,用于内存淘汰。Redis使用LRU(Least Recently Used,最近最少使用)算法来淘汰内存。
但是,Redis并没有使用标准的LRU算法,而是使用了一种近似的LRU算法。为什么呢?因为标准的LRU算法需要维护一个全局的链表,每次访问一个对象都要更新链表,开销太大。
Redis的近似LRU算法是这样的:
- Redis维护一个全局的LRU时钟。
- 每个
redisObject
都有一个lru
字段,记录的是对象上次被访问时,全局LRU时钟的值。 - 当需要淘汰内存的时候,Redis会随机选择一些对象,然后比较它们的
lru
值,选择lru
值最小的对象进行淘汰。
这种算法虽然不是完全精确,但是开销很小,而且效果也比较好。
LRU_BITS:精度和范围
LRU_BITS
定义了lru
字段的位数。位数越多,可以表示的时间范围就越大,精度也就越高。但是,位数越多,占用的空间也就越大。
Redis会根据maxmemory-policy
配置,选择不同的LRU_BITS
:
- 如果
maxmemory-policy
配置为volatile-lru
、allkeys-lru
等LRU策略,LRU_BITS
通常是24位。 - 如果
maxmemory-policy
配置为volatile-ttl
、allkeys-random
等非LRU策略,LRU_BITS
通常是8位。
引用计数:共享的秘密
refcount
字段记录的是对象被引用的次数。当refcount
为0的时候,说明对象没有被任何地方引用,就可以被回收了。
引用计数机制可以有效地节省内存。如果多个地方都用到了同一个对象,Redis不会复制多份,而是让它们都指向同一个对象,然后把refcount
加1。
代码示例:
// 增加对象的引用计数
void incrRefCount(robj *o) {
o->refcount++;
}
// 减少对象的引用计数
void decrRefCount(robj *o) {
if (o->refcount <= 0) return; // 防止出现负数
if (o->refcount == 1) {
// 引用计数为1,说明对象可以被回收了
switch (o->type) {
case REDIS_STRING: freeStringObject(o); break;
case REDIS_LIST: freeListObject(o); break;
case REDIS_HASH: freeHashObject(o); break;
case REDIS_SET: freeSetObject(o); break;
case REDIS_ZSET: freeZsetObject(o); break;
default: break;
}
zfree(o);
} else {
o->refcount--;
}
}
对象共享:小数字的智慧
在Redis启动的时候,会预先创建一些常用的对象,比如0到9999的整数。这些对象会被多个地方共享,从而节省内存。
当需要使用这些整数的时候,Redis会直接返回共享的对象,而不是重新创建一个新的对象。
命令与对象:幕后推手
当我们执行一个Redis命令的时候,Redis会先对输入的参数进行类型检查,然后根据参数的类型,创建相应的redisObject
。
比如,当我们执行SET key value
命令的时候,Redis会创建两个redisObject
,一个表示key
,一个表示value
。
命令执行完毕后,Redis会根据情况,决定是否需要释放这些redisObject
。
总结:Redis对象系统的精髓
Redis的对象系统是Redis高效运行的关键之一。它通过以下手段来提高性能和节省内存:
- 多种数据类型: 支持五种基本数据类型,可以满足不同的应用场景。
- 多种编码方式: 每种数据类型都有多种编码方式,可以根据实际情况选择最合适的编码方式。
- 动态编码转换: 可以根据数据的变化,动态地进行编码转换。
- 近似LRU算法: 使用近似LRU算法来淘汰内存,开销小,效果好。
- 引用计数机制: 通过引用计数机制来实现内存共享,节省内存。
- 对象共享: 预先创建一些常用的对象,供多个地方共享。
理解了Redis的对象系统,就能更好地理解Redis的内部机制,从而更好地使用Redis。
好了,今天的讲座就到这里,希望大家有所收获! 咱们下期再见!