Redis笔记-结构篇-通用对象

前面在讲字典的时候我们曾提到过整个 redis 的 db 中 key-value 结构也是一个 dict,dict 的 key 我们知道是一个字符串 SDS(参照我之前的文章),那么 value 的话可以对应多种类型,为了统一管理 value 的多种类型,redis 提供了一个通用的对象结构 redisObject。

注意:本系列文章分析的 Redis 源码版本:https://github.com/Sidfate/redis ,是文章发布时间的最新版。

前面在讲字典的时候我们曾提到过整个 redis 的 db 中 key-value 结构也是一个 dict,dict 的 key 我们知道是一个字符串 SDS(参照我之前的文章),那么 value 的话可以对应多种类型,为了统一管理 value 的多种类型,redis 提供了一个通用的对象结构 redisObject。

源码结构

redisObject 的源码结构如下:

    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 对象的数据类型。占 4 个位。
encoding 对象的编码。占 4 个位,它的所有取值在上面也给出了。
lru 对象的空转时间。占 24 个位。
refcount 引用计数。它允许 robj 对象在某些情况下被共享。
ptr 数据指针,指向真正的数据。比如,一个代表 string 的 robj,它的 ptr 可能指向一个 sds 结构;一个代表 list 的 robj,它的 ptr 可能指向一个 quicklist。

数据类型 type 其实我们之前有提到的 5 种常用结构,它的取值如下:

    /* 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. */

那么有了 type 为什么还要 encoding 呢,这是因为一种 type 下可能存在多个 encoding 方式。就像我们在 字符串章节中提到的当 type = OBJ_STRING 的时候,表示这个 robj 存储的是一个 string,这时 encoding 可以是下面3种中的一种:

  • OBJ_ENCODING_RAW
  • OBJ_ENCODING_INT
  • OBJ_ENCODING_EMBSTR

通过 encoding 也可以看出来目前 redis 中针对数据类型使用的数据结构,本系列结构篇的源码分析也是从这个角度出发的。以下是 redis 源码中定义的 encoding,大家可以参照之前的文章对应一下 type 和 encoding 的关系 :

    /* Objects encoding. Some kind of objects like Strings and Hashes can be
     * internally represented in multiple ways. The 'encoding' field of the object
     * is set to one of this fields for this object. */
    #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 算法算出来的对象的空闲时间,至于 LRU 算法是什么以及这个字段的具体使用请关注我之后的文章,敬请期待。

接下来我要重点说明下 refcount 引用计数。

refcount 引用计数

如果大家看了之前的文章可以发现 redis 在内存使用上做了太多优化,而很多语言内部针对内存优化都会有自动的内存回收机制,但是 c 语言没有,所以 redis 通过对象的引用计数 refcount 管理自己实现了一套回收机制。简单来说就是判断对象的 refcount 变化来判断是不是需要自动释放对象并进行内存回收。

对象的引用计数信息会随着对象的使用状态而不断变化:

  • 在创建一个新对象时, 引用计数的值会被初始化为 1 ;
  • 当对象被一个新程序使用时, 它的引用计数值会被增一;
  • 当对象不再被一个程序使用时, 它的引用计数值会被减一;
  • 当对象的引用计数值变为 0 时, 对象所占用的内存会被释放。

关于 refcount 这个词其实已经在之前的文章中出现过了,不知道大家发现没,尝试下以下命令:

    > set test_str "abc"
    > debug object test_str
    Value at:0x7fb47bc09150 refcount:1 encoding:embstr serializedlength:4 lru:3567105 lru_seconds_idle:3
    > set test_str 100
    OK
    > debug object test_str
    Value at:0x7fb47bd06320 refcount:2147483647 encoding:int serializedlength:2 lru:3567067 lru_seconds_idle:754

有没有发现奇怪的地方?将 test_str 设置成 “abc” 时我们看到的 refcount 是 1,因为我们新建了这个对象,所以他的 refcount 初始化为 1。但是为什么我们重新将 test_str 设置成 100 后,refcount 的值突然变成了 2147483647,这么大一个数字,原因就在于 100 这个数字。

默认情况下, Redis 会在初始化服务器时,创建一万个字符串对象, 这些对象包含了从 0 到 9999 的所有整数值, 当服务器需要用到值为 0 到 9999 的字符串对象时, 服务器就会使用这些共享对象, 而不是新创建对象。

共享对象带来的就是不需要为这些小整数对象频繁的分配内存,而且不仅可以用到字符串的值,也能用在 dict 中的值,等等。这些小整数对象的引用计数恒等于 INT_MAX,也就是我们所看到的 2147483647。

为什么 Redis 不共享包含字符串的对象?

试想一下,如果我们为一个字符串 “abc” 也创建共享对象,那么如果我想用到它,我就需要在创建新字符串对象的时候比较值是不是与 “abc” 相等,最坏的情况是 O(N),那就会消耗一定的 cpu 时间,如果字符串共享多了就跟不用说了。而整数我们只要比较大小就行了,复杂度只需要 O(1)

虽然共享对象可以减少内存分配,方便管理,但是权衡下来,只共享了整数,况且不好确定需要共享哪些字符串或者非数字对象有哪些。