美文网首页redis
死磕Redis5.0之共享对象

死磕Redis5.0之共享对象

作者: 驶向灯塔的小船 | 来源:发表于2018-11-17 13:36 被阅读156次

          在 Redis 中,内存是很宝贵的资源,我们知道 Redis 之所以快,和它所有数据都在内存中是密不可分的。而内存又是很宝贵的资源,那么 Redis 在使用的内存的时候有没有做什么优化呢?我们一起来探究一下吧。

    redisObject 对象

          为了直观的看到 Redis 存储我们设置的值,我们将从 Redis 的网络模块还是讲起,我们知道 Redis 会将我们设置的值保存在输入缓冲区中,那么我们就来看看输入缓冲区 Redis 做了哪些操作吧。

    /**
     * 从输入缓冲区中读取数据
     */
    int processInlineBuffer(client *c) {
        
        ...
    
        /*
         * Create redis objects for all arguments.
         *
         * todo: 为 client 中所有的参数都创建成一个 redis object 对象
         */
        for (c->argc = 0, j = 0; j < argc; j++) {
            if (sdslen(argv[j])) {
                // 创建一个 object 对象
                c->argv[c->argc] = createObject(OBJ_STRING,argv[j]);
                c->argc++;
            } else {
                sdsfree(argv[j]);
            }
        }
        
        ...
        
    }
    

          从 Redis 的源码我们可以知道,Redis存储的所有值对象在内部定义为redisObject 结构体,具体内部结构体:

    /**
     * Redis 存储的 value 数据都是用 redisObject 来封装的
     * 包括 string,hash,list,set,zset  在内的所有数据类型
     */
    typedef struct redisObject {
        /**
         * 表示当前对象使用的数据类型,
         * Redis主要支持5种数据类型:string,hash,list,set,zset。
         * 可以使用type {key}命令查看对象所属类型,
         * type命令返回的是值对象类型,键都是string类型。
         */
        unsigned type:4;
        /**
         * 表示Redis内部编码类型,encoding在Redis内部使用,
         * 代表当前对象内部采用哪种数据结构实现。
         * 理解Redis内部编码方式对于优化内存非常重要 ,
         * 同一个对象采用不同的编码实现内存占用存在明显差异,
         * 具体细节见之后编码优化部分。
         */
        unsigned encoding:4;
        /**
         * 记录对象最后一次被访问的时间,当配置了
         * maxmemory 和 maxmemory-policy=volatile-lru | allkeys-lru 时,
         * 用于辅助LRU算法删除键数据。
         * 可以使用 object idletime {key} 命令在不更新 lru 字段情况下查看当前键的空闲时间。
         */
        unsigned lru:LRU_BITS; 
        /**
         * 记录当前对象被引用的次数,用于通过引用次数回收内存,
         * 当refcount=0时,可以安全回收当前对象空间。
         * 使用 object refcount {key} 获取当前对象引用。
         */
        int refcount;
        /**
         * 与对象的数据内容相关,如果是整数直接存储数据,否则表示指向数据的指针。
         * Redis在3.0 之后对值对象是字符串且长度 <=39 字节的数据,
         * 内部编码为 embstr 类型,字符串 sds 和 redisObject 一起分配,
         * 从而只要一次内存操作。
         * todo: 因此在高并发的场景尽量是我们的字符串保持 39 字节内,
         * 减少创建redisObject内存分配次数从而提高性能。
         */
        void *ptr;          // 指向底层实现数据结构的指针
    } robj;
    
    

    具体结构内部图如下图所示:


    image.png

          通过上面我们会知道,Redis 会把我们的 value 都用一个 redisObject 对象存储。创建一个 redisObject 对象至少需要 16 个字节。(如果知道 16 个字节是怎么来的大家可以百度一下 c 语言中各个类型所占字节数,这里我只告诉你 type + encoding 占一个字节,lru 占 3 个字节)如果我们总是设置相同的 value,这样 redisObject 就会成倍增长,这样是不是有点浪费内存呢?我们是不是可以把这些对象都共享起来呢?带着这个疑问我们继续往下探索吧。

    Redis 共享对象

          很多人都知道 Redis 内部维护[0-9999]的整数对象池。创建大量的整数类型redisObject 存在内存开销,每个redisObject内部结构至少占16字节,甚至超过了整数自身空间消耗。所以Redis内存维护一个[0-9999]的整数对象池,用于节约内存。 除了整数值对象,其他类型如list,hash,set,zset内部元素也可以使用整数对象池。因此开发中在满足需求的前提下,尽量使用整数对象以节省内存。整数对象池在 Redis 中通过变量 REDIS_SHARED_INTEGERS 定义,不能通过配置修改。
           然而很多人好像也只知道 Redis 会将 [0-9999]的整数对象共享起来,那么除了这些整数之外,Redis 还会创建其他共享对象么?答案是肯定。下面我们就来看看 Redis 到底还维护了哪些对象吧。

    /**
     * todo: Redis 共享变量
     * 共享对象结构体,注意里面每一个共享对象都是 robj(redisObject) 对象
     * 
     * 这里面有部分值是要放到输出缓冲区里面的,为了保证内存中只有一份值,所以
     * 可以将这些对象共享起来,这样可以节约内存。
     */
    struct sharedObjectsStruct {
        robj *crlf, *ok, *err, *emptybulk, *czero, *cone, *cnegone, *pong, *space,
        *colon, *nullbulk, *nullmultibulk, *queued,
        *emptymultibulk, *wrongtypeerr, *nokeyerr, *syntaxerr, *sameobjecterr,
        *outofrangeerr, *noscripterr, *loadingerr, *slowscripterr, *bgsaveerr,
        *masterdownerr, *roslaveerr, *execaborterr, *noautherr, *noreplicaserr,
        *busykeyerr, *oomerr, *plus, *messagebulk, *pmessagebulk, *subscribebulk,
        *unsubscribebulk, *psubscribebulk, *punsubscribebulk, *del, *unlink,
        *rpop, *lpop, *lpush, *zpopmin, *zpopmax, *emptyscan,
        *select[PROTO_SHARED_SELECT_CMDS],
        // todo: 存了 [0, OBJ_SHARED_INTEGERS) 的数字常量
        *integers[OBJ_SHARED_INTEGERS],
        *mbulkhdr[OBJ_SHARED_BULKHDR_LEN], /* "*<value>\r\n" */
        *bulkhdr[OBJ_SHARED_BULKHDR_LEN];  /* "$<value>\r\n" */
        sds minstring, maxstring;
    };
    

    Redis 将所有维护的共享对象都放在 sharedObjectsStruct 结构体中,接下来看看 Redis 是怎么给这些共享对象赋值的吧。

    **
     * Redis 共享变量赋值
     */
    void createSharedObjects(void) {
        ...
        
        // 这里的值都是要放到 Redis 输出缓冲区里面的,要返回给客户端的
        // 所以都是按照 Redis 协议来赋值的
        shared.ok = createObject(OBJ_STRING, sdsnew("+OK\r\n"));
        shared.err = createObject(OBJ_STRING, sdsnew("-ERR\r\n"));
        
        ...
        
        // 客户端发送 ping 命令时,服务端会发送 pong 命令
        shared.pong = createObject(OBJ_STRING, sdsnew("+PONG\r\n"));
        shared.queued = createObject(OBJ_STRING, sdsnew("+QUEUED\r\n"));
        
        ...
        
        // 这里就是数字常量 [0, OBJ_SHARED_INTEGERS)
        for (j = 0; j < OBJ_SHARED_INTEGERS; j++) {
            shared.integers[j] =
                    makeObjectShared(createObject(OBJ_STRING, (void *) (long) j));
            shared.integers[j]->encoding = OBJ_ENCODING_INT;
        }
        
       ...
    }
    
    /**
     * todo: 这里是对 redis 服务器进行初始化
     *
     * [initServer description]
     */
    void initServer(void) {
        
        ...
        
        // todo: 创建一些共享对象
        // Redis 在初始化的时候就会给自己维护的共享对象赋值
        createSharedObjects();
       
        ...
    }
    
    

          上面方法就是给 Redis 维护的所有共享对象赋值,我并没有把所有的共享对象都列出来,如果大家感兴趣可以看看 Redis 源码里面,找到该方法,仔细研究研究。上面我只列出来一些常见的共享对象,看到上面大家应该会很熟悉,因为我们看到了 OK、-ERR、QUEUED 这些常见字符串,大家仔细思考就知道,这些值都是我们设置命令 Redis 给我们返回的响应。是的,Redis 会把一些常见的给客户端回复的字符串共享起来,以此来节省内存。
           讲到这里大家肯定就会有疑问,我们会往 Redis 里面存储很多字符串,这些字符串大多数都是重复的,那么我们把这戏字符串都设置成共享对象,岂不是会节省更多的内存空间?真的是这样吗?思考一下吧(答案:Redis 不会共享包含字符串的对象)

    Why Redis 不共享包含字符串的对象?

          当服务器考虑将一个共享对象设置为键的值对象时,程序需要先检查给定的共享对象和键创建的目标对象是否完全相同,只有在共享对象和目标对象完全相同的情况下,程序才会将共享对象用作键的值对象,而一个共享对象保存的值越复杂,验证共享对象和目标对象是否相同所需的复杂度就会越高,消耗的 CPU 时间也会越多:

    1. 如果共享对象是保存整数值(0~9999)的字符串对象,那么验证操作的复杂度为O(1)
    2. 如果共享对象是保存字符串值的字符串对象,那么验证操作的复杂度为 O(N)
    3. 如果共享对象是包含了多个值(或者对象) 对象,比如列表对象或哈希对象,那么验证操作的复杂度为 O(N^2)

          因此,尽管共享更复杂的对象可以节约更多的内存,但受到 CPU 时间的限制,Redis 只对包含整数值的字符串对象进行共享。

    相关文章

      网友评论

        本文标题:死磕Redis5.0之共享对象

        本文链接:https://www.haomeiwen.com/subject/ljgbfqtx.html