死磕Redis5.0之共享对象

      在 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 只对包含整数值的字符串对象进行共享。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354

推荐阅读更多精彩内容

  • 转载:可能是目前最详细的Redis内存模型及应用解读 Redis是目前最火爆的内存数据库之一,通过在内存中读写数据...
    jwnba24阅读 623评论 0 4
  • 转载:可能是目前最详细的Redis内存模型及应用解读 Redis是目前最火爆的内存数据库之一,通过在内存中读写数据...
    meng_philip123阅读 1,440评论 1 22
  • 妈,您再也不用吃药,再也不用打针,再也没有病痛,可是女儿想您,撕声裂肺呼唤妈妈,您再也不会答应。 您一生操碎了心,...
    保持微笑_8cbf阅读 664评论 1 3
  • 今天我和全湘涵等几个小伙伴去游泳班学习游泳了。这次我们去学习游泳是非常紧张的,为什么呢?因为明天就要考试了。我们来...
    勤_310b阅读 224评论 0 0
  • 冬来了,风起了, 走在成都的街头,还不太冷, 行色匆匆的行人, 我也只是一个行人。 你裹着棉服,她还露着脖颈, 我...
    一叶知秋n阅读 272评论 0 1