REDIS的data type

 在REDIS中,KV是基本的存储概念,而VALUE支持的type主要为5种:STRING,LIST,SET,ZSET(SORTED SET),HASH。本文将详细介绍这些结构的实现。

 首先 这5种类型的宏定义可以在redis.h中找到


#define REDIS_STRING 0
#define REDIS_LIST 1
#define REDIS_SET 2
#define REDIS_ZSET 3
#define REDIS_HASH 4
 

 其次,在reid的内存结构中,上面的5种类型都是采用一样的底层内存结构,—redisObject

typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
    int refcount;
    void *ptr;
} robj;

 

 也就是说,最上述的5种类型,对应的是redisObject中的type字段,refcount则是指的是此值的被引用的次数。ptr则是对应具体的值的指针。从这点而言,redis采用了PHP类似的ZVAL的存储结  构。而且,在最底层redis采用的是计数回收机制。

 

   

void decrRefCount(robj *o) {
    if (o->refcount <= 0) redisPanic("decrRefCount against refcount <= 0");
    if (o->refcount == 1) {
        switch(o->type) {
        case REDIS_STRING: freeStringObject(o); break;
        case REDIS_LIST: freeListObject(o); break;
        case REDIS_SET: freeSetObject(o); break;
        case REDIS_ZSET: freeZsetObject(o); break;
        case REDIS_HASH: freeHashObject(o); break;
        default: redisPanic("Unknown object type"); break;
        }
        zfree(o);
    } else {
        o->refcount--;
    }

  

 熟悉PHP的人看到这样的结构以及这样的逻辑应该知道,此处有memeory leak的风险:如果有循环引用,这样的回收算法是必定有memory leak的(详见http://php.net/manual/en/features.gc.refcounting-basics.php)。PHP的解决方案在此不表。redis的解决方案就是:refcount被修改只有两种情况:初始化;STRING或者long被重用。也就是说,redis不支持redisObject的互相引用,也就是说,不会存在引用环的问题,也就解决了基于计数回收的memoryleak问题。

 下面说<type,encoding,ptr>这三个元素构成的tulple。在redisObject中(下文皆简称robj),ptr是void类型,是因为对于不同的类型以及encoding,ptr的结构是不一样的。具体的encoding详见

#define REDIS_ENCODING_RAW 0     /* Raw representation */
#define REDIS_ENCODING_INT 1     /* Encoded as integer */
#define REDIS_ENCODING_HT 2      /* Encoded as hash table */
#define REDIS_ENCODING_ZIPMAP 3  /* Encoded as zipmap */
#define REDIS_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list */
#define REDIS_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define REDIS_ENCODING_INTSET 6  /* Encoded as intset */
#define REDIS_ENCODING_SKIPLIST 7  /* Encoded as skiplist */
#define REDIS_ENCODING_EMBSTR 8  /* Embedded sds string encoding */

 

 其中,REDIS_ENCODING_INT,REDIS_ENCODING_EMBSTR和EDIS_ENCODING_RAW对应的是STRING类型。换言之,redis没有原生的number类型。其他的encoding和type对应关系则是为了优化和持久化的考虑,详细为:

    • <REDIS_LIST,REDIS_ENCODING_LINKEDLIST>

    • <REDIS_LIST,REDIS_ENCODING_ZIPLIST>

    • <REDIS_SET,REDIS_ENCODING_HT>

    • <REDIS_SET,REDIS_ENCODING_INTSET>

    • <REDIS_ZSET,REDIS_ENCODING_SKIPLIST>

    • <REDIS_ZSET,REDIS_ENCODING_ZIPLIST>

    • <REDIS_HASH,REDIS_ENCODING_HT>

    • <REDIS_HASH,REDIS_ENCODING_ZIPLIST>

 REDIS_ENCODING_ZIPMAP在源代码中没找到涉及到的地方,后来在官网发现这个encoding已经在2.6之后被remove掉了(参见http://redis.io/topics/memory-optimization)。对于同一种type,什么条件下会采用不同的encoding,下文会详述。

 首先说STRING。STRING存储redis中最普通的数据:string,number,char。而其结构名称为sds,其定义为

 

typedef char *sds;

 

 显然,sds只是一个char数组的指针。但是如果仅仅是数组的话,完全不满足redis的关于string的操作,比如append。于是redis又包装了另外一个结构

 

  

struct sdshdr {
    /* 值长度 */
    unsigned int len;
    /* 未使用长度 */
    unsigned int free;
    /* 值 */
    char buf[];
};

 

 故而,在redis内部,都是采用sdshds这个结构存储。每一次需要增加容量的时候,redis首先是得到需要追加的长度,然后采取策略将当前的char数组扩大,继而是写入,最后将尾部的空白清楚。具体步骤为

    • 计算所需增加的长度

    • 为新的buff[]开辟空间。具体是采用翻倍策略——如果当前长度小于1024*1024,则翻倍,否则增加1024*1024,更新free字段

    • 将新的内容写入,并在最后添加\0作为结束符,同时更新len字段

 这里有个细节问题,就是在append后,会存在空余空间,也就是说,free不再是0了。那下次如果再append的时候会如何?redis采取的策略是,——每次计算所需增加的长度的时候,是基于len这个字段增加的。于是redis最多空余len-1个长度的空间。同时,由于redis接受client的命令以及返回的时候都是采用sds结构,这就造成了在返回的时候很容易造成很大的free空间,而向client返回这个操作相对于内存操作而言耗时太长,于是在返回前会对返回内容进行空白回收,也就是sdsRemoveFreeSpace方法。

 在redis中,所有sds的操作都可以在sds.c文件中找到,主要提供了以下的功能(下表参考于http://redisbook.readthedocs.org/en/latest/internal-datastruct/sds.html。若涉及任何版权问题,请联系本人)

 

函数 作用 算法复杂度
sdsnewlen 创建一个指定长度的 sds ,接受一个 C 字符串作为初始化值 O(N)
sdsempty 创建一个只包含空白字符串 "" 的 sds O(1)
sdsnew 根据给定 C 字符串,创建一个相应的 sds O(N)
sdsdup 复制给定 sds O(N)
sdsfree 释放给定 sds O(N)
sdsupdatelen 更新给定 sds 所对应 sdshdr 结构的 free 和 len O(N)
sdsclear 清除给定 sds 的内容,将它初始化为 "" O(1)
sdsMakeRoomFor 对 sds 所对应 sdshdr 结构的 buf 进行扩展 O(N)
sdsRemoveFreeSpace 在不改动 buf 的情况下,将 buf 内多余的空间释放出去 O(N)
sdsAllocSize 计算给定 sds 的 buf 所占用的内存总数 O(1)
sdsIncrLen 对 sds 的 buf 的右端进行扩展(expand)或修剪(trim) O(1)
sdsgrowzero 将给定 sds 的 buf 扩展至指定长度,无内容的部分用 \0 来填充 O(N)
sdscatlen 按给定长度对 sds 进行扩展,并将一个 C 字符串追加到 sds 的末尾 O(N)
sdscat 将一个 C 字符串追加到 sds 末尾 O(N)
sdscatsds 将一个 sds 追加到另一个 sds 末尾 O(N)
sdscpylen 将一个 C 字符串的部分内容复制到另一个 sds 中,需要时对 sds 进行扩展 O(N)
sdscpy 将一个 C 字符串复制到 sds O(N)

 而涉及到的所有内存空间开辟都有唯一的信号量控制。其类型为pthread_mutex_t。

 

 下面讲一下string对于number的支持。

 redis对于String的赋值前,会调用tryObjectEncoding方法去检查应该采用什么样的编码。为了方便,在此贴出尝试转换为int的代码细节:

 

	
len = sdslen(s);
        /* power(10,20) <power(2,64) < power(10,21) */
    if (len <= 21 && string2l(s,len,&value)) {
        /* This object is encodable as a long. Try to use a shared object.
         * Note that we avoid using shared integers when maxmemory is used
         * because every object needs to have a private LRU field for the LRU
         * algorithm to work well. */
        if ((server.maxmemory == 0 ||
             (server.maxmemory_policy != REDIS_MAXMEMORY_VOLATILE_LRU &&
              server.maxmemory_policy != REDIS_MAXMEMORY_ALLKEYS_LRU)) &&
            value >= 0 &&
            value < REDIS_SHARED_INTEGERS)
        {
            decrRefCount(o);
            incrRefCount(shared.integers[value]);
            return shared.integers[value];
        } else {
            if (o->encoding == REDIS_ENCODING_RAW) sdsfree(o->ptr);
            o->encoding = REDIS_ENCODING_INT;
            o->ptr = (void*) value;
            return o;
        }
    }
 

 显然,判断是否要转为int的条件有两个:len不超过21(此时传过来的还是字符型,也就是"123");转换成功(有些废话)。如果上述两个条件成立,则做一些后续工作(因为已经转换成功了)。其中shared是一个内存宏观变量,可以认为是共享常量池。在此我们只关心integer常量池。从代码完全可以看出,此integer常量池是一个数组,数组长度由内存参数REDIS_SHARED_INTEGERS指定,或者说,是一个0到REDIS_SHARED_INTEGERS的常量桶。也就是说,如果发现这个值是在桶里面的,则无需开辟空间,直接指向这个值就行了。如果不是,则直接将该值的指针指向这个转换过来的值(按照64位的long为8byte)。其中string2l中转换数字的算法比较漂亮,在此粘贴一下(算是仰慕一下大神):

 

 

while (plen < slen && p[0] >= '0' && p[0] <= '9') {
        if (v > (ULLONG_MAX / 10)) /* Overflow. */
            return 0;
        v *= 10;
 
        if (v > (ULLONG_MAX - (p[0]-'0'))) /* Overflow. */
            return 0;
        v += p[0]-'0';
 
        p++; plen++;
 }

 

 看了下前面写的内容,感觉本文写不完所有的data type了。干脆就讲下string的优化策略。在string中,存在一个EMB_STR的encoding,也就是说,在最终存值之前,会尝试是否能将string以EMB_STR的格式存储,具体代码如下:

 

 

if (len <= REDIS_ENCODING_EMBSTR_SIZE_LIMIT) {
        robj *emb;
 
        if (o->encoding == REDIS_ENCODING_EMBSTR) return o;
        emb = createEmbeddedStringObject(s,sdslen(s));
        decrRefCount(o);
        return emb;
 }

 

 从代码中可以看到,当整个string的长度少于参数REDIS_ENCODING_EMBSTR_SIZE_LIMIT(默认是39)的时候,会将此STR重新编码。而编码的方式也是比较简单:

 

	
robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr)+len+1);
    struct sdshdr *sh = (void*)(o+1);
 
    o->type = REDIS_STRING;
    o->encoding = REDIS_ENCODING_EMBSTR;
    o->ptr = sh+1;
    o->refcount = 1;
    o->lru = LRU_CLOCK();
 
    sh->len = len;
    sh->free = 0;
    if (ptr) {
        memcpy(sh->buf,ptr,len);
        sh->buf[len] = '\0';
    } else {
        memset(sh->buf,0,len+1);
    }

 

 如果要阐述为什么会如此优化,首先得说明一下一个robj的分配机制。要创建一个robj对象,第一步是基于robj的struct分配内存空间,然后根据值的长度分配对应的ptr空间。这个在前文也大致说过。但是对于embstr的方式,不是这样,他是将两次的内存分配合并成一个。也就是上述代码的第一行

robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr)+len+1);
 

 也就是说,省去了一次和内核打交道的过程以及减少一次pthread_mutex_t的操作。但是带来的问题也比较明显,由于是一次性分配,他的robj的内存和ptr的内存是放在一块儿的(o->ptr = sh+1)。也就是说,如果这个string发生变化了,就意味着在整个robj的内存块重新规划内存。这显然是很不方便的,这也就是为何上述代码的注释为(注意烈面的单词unmodified):

 
/* Create a string object with encoding REDIS_ENCODING_EMBSTR, that is
 * an object where the sds string is actually an unmodifiable string
 * allocated in the same chunk as the object itself. */

 

 那么为何要设置一个阀值REDIS_ENCODING_EMBSTR_SIZE_LIMIT呢?很简单,内容越小,修改的需求越少,长度变更的可能性也越小。

 

From:鱼藏

 

你可能感兴趣的:(REDIS的data type)