Redis对象

机会只留给那些准备充分的人

Posted by irving.gx on April 17, 2020

  我们在前面的文章当中分析了redis的几种数据结构,在这篇文章当中我们来分析一下redis对象。

Redis对象结构

我们首先来看一下redis对象的源码:

可以看到redis对象当中有一个成员变量是type,用于标识对象的类型,一共有5种类型,分别是string(字符串),list(列表),hash(哈希对象),set(集合), zset(有序集合)。第二个成员变量encoding,表示编码方式,每种数据类型有不同的编码方式,具体的类型以及编码方式与意义可以参见下表

类型 编码 对象
OBJ_STRING OBJ_ENCODING_INT 使用整数实现的字符串对象
OBJ_STRING OBJ_ENCODING_EMBSTR 使用embstr编码实现的字符串对象
OBJ_STRING OBJ_ENCODING_RAW 使用sds实现的字符串对象
OBJ_LIST OBJ_ENCODING_QUICKLIST 使用quicklist实现的列表对象
OBJ_HASH OBJ_ENCODING_ZIPLIST 使用压缩表实现的hash对象
OBJ_HASH OBJ_ENCODING_HT 使用字典实现的hash对象
OBJ_SET OBJ_ENCODING_INTSET 使用整数集合实现的集合对象
OBJ_SET OBJ_ENCODING_HT 使用字典实现的集合对象
OBJ_ZSET OBJ_ENCODING_ZIPLIST 使用压缩列表实现的有序集合对象
OBJ_ZSET OBJ_ENCODING_SKIPLIST 使用跳跃表和字典实现的有序集合对象

redis对象的第三个成员变量lru用于记录lru(least recently used)的时钟,这个字段用于redis的缓存以及淘汰策略。 refcount记录该对象的引用次数,redis的垃圾回收策略采用的是引用计数法,所以要知道每个对象的引用数目。redis对象当中ptr保存的是指向底层数据结构的指针。

对象创建

我们看一下redis对象创建的代码

从上面的代码中我们可以看到在创建一个新的对象的时候先分配内存,然后传入对象的类型,设置OBJ_ENCODING_RAW的编码方式,同时设置引用数目为1, 在设置lru字段的时候会做一下判断,判断采用的是LFU(Least Frequently Used)的策略还是LRU(Least Recently Used)的策略。LFU一般会采 用一种计数的方式,如果一个key被访问的次数很多那么如果采用的是LFU的策略的话那这个key就不容易被淘汰。如果是采用的LRU的方式,就会在创建对象的 时候设置LRU时钟。

字符串对象

我们来看一下创建redis字符串对象的代码

可以看到在创建字符串对象的时候是根据字符串的长度大小来分别执行不同的逻辑,如果长度小于44就采用创建嵌入字符串的方式,否则就采用创建原生字符串的方 式。那么这里为什么以44作为分界呢,源码当中有注释是这样说的

The current limit of 44 is chosen so that the biggest string object * we allocate as EMBSTR will still fit into the 64 byte arena of jemalloc.

也就是说为了让这个字符串对象能够使用jemalloc,让总的大小限定在64 byte。因为字符串会采用sdshdr8来保存,这个结构体需要3个字节, Object结构体需要16个字节,因此是64-16-3=45,而且字符串的结尾统一是\0,要再减去一个字节,因此是44个字节的长度。

我们看一下当字符串长度小于44的时候的逻辑,

第85行我们可以看到,在申请内存的时候,同时申请了字符串对象以及存储的sds的内存大小,这样就将内存申请从2次变成了1次,而且释放的时候也 只需要释放一次,并且字符串对象与动态字符串的内存是连续的。接下来的代码就是对于这个对象进行初始化,将编码方式变为OBJ_ENCODING_EMBSTR.

   我们再看一下当字符串长度大于44的时候的逻辑

可以看到如果是大于44的话,就直接调用了createObject的方法,将编码方式设置为OBJ_ENCODING_RAW。

其他对象的创建

其他几种对象的创建源码如下所示

基本上都是先申请内存空间,然后调用createObject方法来进行创建,具体细节不在这里详述。

引用计数

当一个对象被创建时,引用计数会被初始化为1,我们回顾一下创建对象的时候对于引用计数的操作

在46行我们可以看到,在创建一个对象的时候会把该对象的refcount字段的值置为1。如果对于对象重复使用的话会对于对象的这个值减一, 我们看一下源码当中setKey方法,该方法是对于一个key的值进行设置,添加一个新的key或者是对于原来的key的value进行覆盖。在第222行我们可以看到, 调用了incrRefCount这个方法。

我们看一下这个方法的代码

其中OBJ_SHARED_REFCOUNT的值为INT_MAX,可以看到在这里对象的refcount字段自增了1。当对象不再被使用时,会调用decrRefCount对该对象的refcount值减一 ,我们看一下decrRefCount方法

可以看到如果对象的引用值只有1,而且又调用了该方法的话,那么会根据对象的类型对于对象占用的内存空间进行释放,我们以set对象为例,会调用 freeSetObject这个方法,该方法如下:

可以看到对于一个对象的释放是根据对象的编码方式采取不同的方法,以set对象为例,如果该对象是OBJ_ENCODING_HT编码方式的,也就是用字典实现的集合对象。 用字典实现集合的话是把存储的内容放在key里面,而把value置为null,我们看下这种编码方式的释放代码

可以看到会对于ht[0]以及ht[1]进行释放,然后再调用zfree方法。如果编码方式是OBJ_ENCODING_INTSET的话,会直接调用zfree方法对于内存进行释放。

LRU

在上一篇当中我们了解到一个对象的lru字段记录的值会因采用的策略而不同,如果是LRU策略的话那么就会在创建的时候设置LRU时钟,在每次正常访问数据的时候 就会设置对应的LRU时钟,我们来看一个例子

这个方法是查找一个key,从第67-70行我们可以看到,当采用的LFU策略的时候,会更新对应LFU的值,如果采用的是LRU策略的话那么就会在lru字段当中设 置LRU时钟。

        以上是对于redis对象的一点简单分析。


如果对你有帮助,请作者喝一杯牛奶吧