redisObject
Redis并没有直接使用前面小节所讲的那些数据结构来实现键值对数据库,而是基于那些数据结构抽象除了一套对象系统。我们熟知的字符串、列表、哈希、集合等数据结构,实际上对应着一个个的对象。
Redis中的每个对象都由一个 redisObject 结构表示:
typedef struct redisObject {
unsigned type:4;
unsigned encoding;
void *prt;
// ...
} robj;
接下来,详细讲解一下这些属性:
type(类型)
type属性记录了对象的类型,可以是一下常量值中的任意一种:
{
"REDIS_STRING":"字符串对象",
"REDIS_LIST":"列表对象",
"REDIS_HASH":"哈希对象",
"REDIS_SET":"集合对象",
"REDIS_ZSET":"有序集合对象"
}
(以上是伪代码,用json来表示是为了更好的对应)
键值对的键总是一个字符串对象,即type类型为REDIS_STRING。
键值对的值可以是任意的对象。我们表示某个键的时候,比如说集合键,实际上是在说它的值的类型。
encoding(编码)和*ptr(底层实现)
有了type,我们已经可以知道具体的类型了,为什么还要搞一个编码?
因为同一个类型的对象,其底层的实现不一定相同。每种类型的对象至少使用了两种不同的编码。
ptr指针根据encoding的属性值,指向对应的底层数据结构。
字符串对象
字符串对象的编码可以是int、embstr、raw。
不同编码的应用场景?
int:存储的是一个long类型的整数值
embstr:存储的是一个长度小于等于39字节的字符串
raw:存储的是一个长度大于39字节的字符串(SDS)
embstr?
embstr编码是专门用于存储字符串的一种优化方式。
上文我们知道,字符串对象用一个redisObject结构体表示,其ptr指针指向对应的底层数据结构,其实就是一个sdshdr结构体。但是,与raw不同的是,embstr只需要一次内存分配,因为redisObject和sdshdr放在统一内存块中。这样就可以很好的去利用局部性原理,提高数据的处理速度。
最后,还要说的是,如果存储的是一个浮点数,那么在底层会转成字符串进行存储(embstr或者raw),对浮点数进行操作时,先转成浮点数,再执行相关操作。
不同的编码类型怎么进行转化?
满足对应编码的存储条件就行了。但是,embstr实际上是只读的。对embstr编码的字符串进行修改操作时,会先转成raw类型的字符串,再进行修改操作。所以,emstr执行修改操作后,总是返回一个raw编码的字符串对象。
列表对象
列表对象的编码可以是ziplist和linkedlist。
ziplist:字符串元素的长度小于64字节且数量小于512。
linkedlist:不满足ziplist,即是。
哈希对象
哈希对象的编码可以是ziplist和hashtable。
ziplist:键值对的键和值长度都小于64字节且数量小于512.
hashtable:不满足ziplist,即是。
也许你会有这样的疑问:列表查找元素的时间复杂度不是O(N)嘛?为什么还要使用ziplist?
- ziplist是内存紧凑的,可以很好的利用局部性原理,加快对访问速度。
- 列表的长度较小时,遍历查找的速度甚至比进行哈希计算的速度要快。
集合对象
集合对象的编码类型可以使intset或者hashtable。
intset:元素是整数且数量不超过512。
hashtable:不满足intset,即是。
有序集合对象
有序集合对象的编码可以是ziplist和skiplist。
ziplist:元素长度小于64字节且数量小于128。
skiplist:不满足ziplist,即是。
光使用skiplist,某些操作的时间复杂度无法做到O(1),比如检测某个元素是否在集合中,所以内部其实采用了 skiplist + hashtable。
内存回收
Redis使用C原语言实现,C语言自身并不具备自动化的垃圾回收机制。
Redis采用引用计数机制来进行垃圾回收。
typedef struct redisObject {
// ...
int refCount;
// ...
} robj;
refCount属性代表引用的数量。当refCount的值为0时,代表该对象被标记为可回收。
引入引用计数机制还有一个好处就是可以共享对象。但是大多数情况下,都不会去共享。
想一下,共享的前提是什么?是不是新插入的键的值要在原由的数据库中存在,而且相等。判断两个对象是否相等,对象的结构越复杂,耗时也就越长。所以,Redis只会共享包含整数的字符串对象。
对象的空转时间
typedef struct redisObject {
// ...
unsigned lru:22;
// ...
} robj;
lru属性记录了该对象上一次被访问的时间戳。
我们可以利用lru属性值计算出对象的空转时间:空转时间 = 当前时间 - lru的值。
空转时间对于Redis的一些策略有重要的意义,后面的章节会详细的叙述。
2024.10.23
writeBy kaiven