Redis-String类型

栏目: 数据库 · 发布时间: 5年前

内容简介:编码方式为raw时,robj中的ptr指向一个sds结构中字符串的起始位置。sds结构分为两个部分,头部由len,alloc,flags组成,尾部是buf,存储字符串数据的地方。比如存储一个“value”,会是下面的结构:

编码方式为raw时,robj中的ptr指向一个sds结构中字符串的起始位置。

sds结构分为两个部分,头部由len,alloc,flags组成,尾部是buf,存储字符串数据的地方。

比如存储一个“value”,会是下面的结构:

Redis-String类型

当存储的字节数小于OBJ_ENCODING_EMBSTR_SIZE_LIMIT(写博客的时候是44)时,可能会被转换成embstr编码。

embstr

编码为embstr的时候,字符串同样是用sds结构存储的,不过此时是紧接着robj对象之后的。

示意图如下:

Redis-String类型

int

int类型的编码就更简单了,存储整数可以说是最省空间的了,直接就把整数的值放在了robj的ptr属性里面。

Redis-String类型

当把String类型的值当成数字来操作,并且值的编码不是int的时候,redis会把String编码自动转换成int(如果没有错误的话)。

代码分析

通过下面的代码分析,可以了解 redisset key value 以及 incr 命令的执行流程,以及各种编码在redis中的自动转换。

set命令

下载redis源码,编译,gdb,然后 b setCommand 在setCommand处打一个断点。

启动客户端,添加一个新的键值对 set key value ,服务器端停在了断点处,backtrace:

(gdb) backtrace
#0  setCommand (c=0x7ffff691ed00) at t_string.c:96
#1  0x000000000042bf86 in call (c=c@entry=0x7ffff691ed00, flags=flags@entry=15) at server.c:2229
#2  0x000000000042c76f in processCommand (c=0x7ffff691ed00) at server.c:2515
#3  0x000000000043c4f5 in processInputBuffer (c=0x7ffff691ed00) at networking.c:1357
#4  0x0000000000425f0d in aeProcessEvents (eventLoop=eventLoop@entry=0x7ffff683c0a0, flags=flags@entry=11)
    at ae.c:443
#5  0x000000000042613b in aeMain (eventLoop=0x7ffff683c0a0) at ae.c:501
#6  0x0000000000422c56 in main (argc=<optimised out>, argv=0x7fffffffd0f8) at server.c:3899
(gdb)

如果继续往下执行会依次执行 setGenericCommand -> setKey-> dbAdd -> dictAdd ,在查看了代码之后发现 setGenericCommand, setKey, dbAdd 的函数参数中都有 robj* val ,猜测在这之前作为值的robj对象就已经构建好了,经过自己向后追踪,确实是这样。

setCommand 调用setGenericCommand的时候是如下调用的:

setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);

而setGenericCommand的原型:

void setGenericCommand(client *c, int flags, robj *key, robj *val, robj *expire, int unit, robj *ok_reply, robj *abort_reply);

值对象是在setCommand之前被构建并且存入到c->argv[2]中的,c->argv[1]中的是键,而c->argv[0]中的是命令。

经过查找,发现了构建位置, processInputBuffer->processInlineBuffer->createObject

关键点一:解析接收到的参数

在processInlineBuffer中,服务器接收到的参数会被解析成字符串数组。

来看 processInlineBuffer

processInputBuffer: int processInlineBuffer(client *c) {
    char *newline;
    int argc, j, linefeed_chars = 1;
    sds *argv, aux;
    size_t querylen;

    // 省略一些代码,只看如何生成值对象的

    argv = sdssplitargs(aux,&argc);	
	// aux是所有参数组成的字符串,类型为sds
	// split之后得到了argv字符串数组
    sdsfree(aux);
    if (argv == NULL) {
        addReplyError(c,"Protocol error: unbalanced quotes in request");
        setProtocolError("unbalanced quotes in inline request",c,0);
        return C_ERR;
    }

	// 省略代码。。。。

    /* Setup argv array on client structure */
	// 上面split之后获得了参数的个数argc
    if (argc) {
        if (c->argv) zfree(c->argv);
        c->argv = zmalloc(sizeof(robj*)*argc);
    }

    /* Create redis objects for all arguments. */
	// 从参数字符数组argv中构建robj数组,存储到client->argv中
    for (c->argc = 0, j = 0; j < argc; j++) {
        if (sdslen(argv[j])) {
            c->argv[c->argc] = createObject(OBJ_STRING,argv[j]);
            c->argc++;
        } else {
            sdsfree(argv[j]);
        }
    }
    zfree(argv);
    return C_OK;
}

在上面的processInlineBuffer中,把接收到的参数使用sdssplitargs分离开来,生成一个字符串数组。

关键点二:参数转为robj

在processInlineBuffer的稍后部分,对于字符串数组中的每一个元素,都生成了一个robj对象,这些对象都是String型,且编码为raw。

然后对每个sds调用createObject生成robj对象,存储到client->argv数组中。

createObject代码:

robj *createObject(int type, void *ptr) {
    robj *o = zmalloc(sizeof(*o));
    o->type = type;
    o->encoding = OBJ_ENCODING_RAW;
    o->ptr = ptr;
    o->refcount = 1;

    /* Set the LRU to the current lruclock (minutes resolution), or
     * alternatively the LFU counter. */
    if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
        o->lru = (LFUGetTimeInMinutes()<<8) | LFU_INIT_VAL;
    } else {
        o->lru = LRU_CLOCK();
    }
    return o;
}

关键点三:raw转embstr/int

在之前提到的 setCommand 函数中,调用 setGenericCommand 之前,有如下代码:

void setCommand(client* c) {
    // pass
    c->argv[2] = tryObjectEncoding(c->argv[2]);
	setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);
}

通过上面的分析,我们知道c->argv[2]就是输入命令 set key value 中的“value”, tryObjectEncoding 把这个原本是raw编码的String类型对象转换成了embstr编码。

转换代码如下:

/* Try to encode a string object in order to save space */
robj *tryObjectEncoding(robj *o) {
    long value;
    sds s = o->ptr;
    size_t len;

	// 确保为String类型
    serverAssertWithInfo(NULL,o,o->type == OBJ_STRING);

    // 一个宏
    //./server.h:1480:
    //#define sdsEncodedObject(objptr) (objptr->encoding == OBJ_ENCODING_RAW || objptr->encoding == OBJ_ENCODING_EMBSTR)
    if (!sdsEncodedObject(o)) return o;

    /* It's not safe to encode shared objects: shared objects can be shared
     * everywhere in the "object space" of Redis and may end in places where
     * they are not handled. We handle them only as values in the keyspace. */
     if (o->refcount > 1) return o;

    /* Check if we can represent this string as a long integer.
     * Note that we are sure that a string larger than 20 chars is not
     * representable as a 32 nor 64 bit integer. */
    // 如果可以转换成int,则转换成int编码,然后返回
    len = sdslen(s);
    if (len <= 20 && 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 & MAXMEMORY_FLAG_NO_SHARED_INTEGERS)) &&
            value >= 0 &&
            value < OBJ_SHARED_INTEGERS)
        {
            decrRefCount(o);
            incrRefCount(shared.integers[value]);
            return shared.integers[value];
        } else {
            if (o->encoding == OBJ_ENCODING_RAW) sdsfree(o->ptr);
            o->encoding = OBJ_ENCODING_INT;
            o->ptr = (void*) value;
            return o;
        }
    }

    /* If the string is small and is still RAW encoded,
     * try the EMBSTR encoding which is more efficient.
     * In this representation the object and the SDS string are allocated
     * in the same chunk of memory to save space and cache misses. */
    // 尝试转换成embstr
    if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT) {
        robj *emb;

        if (o->encoding == OBJ_ENCODING_EMBSTR) return o;
        emb = createEmbeddedStringObject(s,sdslen(s));
        decrRefCount(o);
        return emb;
    }

    /* We can't encode the object...
     *
     * Do the last try, and at least optimize the SDS string inside
     * the string object to require little space, in case there
     * is more than 10% of free space at the end of the SDS string.
     *
     * We do that only for relatively large strings as this branch
     * is only entered if the length of the string is greater than
     * OBJ_ENCODING_EMBSTR_SIZE_LIMIT. */
    if (o->encoding == OBJ_ENCODING_RAW &&
        sdsavail(s) > len/10)
    {
        o->ptr = sdsRemoveFreeSpace(o->ptr);
    }

    /* Return the original object. */
    return o;
}

raw转int的过程都在上面的代码中了,而转embstr需要的函数createEmbeddedStringObject如下:

/* Create a string object with encoding OBJ_ENCODING_EMBSTR, that is
 * an object where the sds string is actually an unmodifiable string
 * allocated in the same chunk as the object itself. */
robj *createEmbeddedStringObject(const char *ptr, size_t len) {
    // 从分配的空间即可看出,robj和字符串数据是在一起的
    robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr8)+len+1);
    struct sdshdr8 *sh = (void*)(o+1);

    o->type = OBJ_STRING;
    o->encoding = OBJ_ENCODING_EMBSTR;
    o->ptr = sh+1;	// ptr指向字符串数据第一个字符
    o->refcount = 1;
    if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
        o->lru = (LFUGetTimeInMinutes()<<8) | LFU_INIT_VAL;
    } else {
        o->lru = LRU_CLOCK();
    }

    sh->len = len;
    sh->alloc = len;
    sh->flags = SDS_TYPE_8;
    if (ptr) {
        memcpy(sh->buf,ptr,len);
        sh->buf[len] = '\0';
    } else {
        memset(sh->buf,0,len+1);
    }
    return o;
}

incr命令

incr的调用流程如下:

(gdb) backtrace
#0  incrDecrCommand (c=0x7ffff691ed00, incr=1) at t_string.c:345
#1  0x000000000042bf86 in call (c=c@entry=0x7ffff691ed00, flags=flags@entry=15) at server.c:2229
#2  0x000000000042c76f in processCommand (c=0x7ffff691ed00) at server.c:2515
#3  0x000000000043c4f5 in processInputBuffer (c=0x7ffff691ed00) at networking.c:1357
#4  0x0000000000425f0d in aeProcessEvents (eventLoop=eventLoop@entry=0x7ffff683c0a0, flags=flags@entry=11)
    at ae.c:443
#5  0x000000000042613b in aeMain (eventLoop=0x7ffff683c0a0) at ae.c:501
#6  0x0000000000422c56 in main (argc=<optimised out>, argv=0x7fffffffd0f8) at server.c:3899
(gdb)

其实incr和decr命令是分别调用对应的incrCommand和decrCommand函数,不过背后还是incrDecrCommand这个函数。

关键点一:incr无法对超过long long的整数进行自增操作

从下面incrDecrCommand代码可以看出,自增是把raw型或这embstr型编码的字符串转换成long long,然后增加,如果超过这个范围,无法转换,自然无法自增。

void incrDecrCommand(client *c, long long incr) {
    long long value, oldvalue;
    robj *o, *new;

    // 获取键对应的值
    o = lookupKeyWrite(c->db,c->argv[1]);
    
    // 不是String类型,类型不匹配
    if (o != NULL && checkType(c,o,OBJ_STRING)) return;
    
    // 尝试转换,若无法转换,如不是整形,或者溢出,返回错误。
    if (getLongLongFromObjectOrReply(c,o,&value,NULL) != C_OK) return;

    // 递增
    oldvalue = value;
    if ((incr < 0 && oldvalue < 0 && incr < (LLONG_MIN-oldvalue)) ||
        (incr > 0 && oldvalue > 0 && incr > (LLONG_MAX-oldvalue))) {
        addReplyError(c,"increment or decrement would overflow");
        return;
    }
    value += incr;

    // 下面是写回
    if (o && o->refcount == 1 && o->encoding == OBJ_ENCODING_INT &&
        (value < 0 || value >= OBJ_SHARED_INTEGERS) &&
        value >= LONG_MIN && value <= LONG_MAX)
    {
        new = o;
        o->ptr = (void*)((long)value);
    } else {
        // ********注意这里***********
        // 这个函数会基于给定的long long值构造一个String类型的对象
        new = createStringObjectFromLongLong(value);
        if (o) {
            dbOverwrite(c->db,c->argv[1],new);
        } else {
            dbAdd(c->db,c->argv[1],new);
        }
    }
    signalModifiedKey(c->db,c->argv[1]);
    notifyKeyspaceEvent(NOTIFY_STRING,"incrby",c->argv[1],c->db->id);
    server.dirty++;
    addReply(c,shared.colon);
    addReply(c,new);
    addReply(c,shared.crlf);
}

我们还可以看下createStringObjectFromLongLong这个函数,如果超出范围,将返回embstr编码的对象,没超则返回int编码对象:

// object.c
robj *createStringObjectFromLongLong(long long value) {
    robj *o;
    if (value >= 0 && value < OBJ_SHARED_INTEGERS) {
        incrRefCount(shared.integers[value]);
        o = shared.integers[value];
    } else {
        if (value >= LONG_MIN && value <= LONG_MAX) {
            o = createObject(OBJ_STRING, NULL);
            o->encoding = OBJ_ENCODING_INT;	// 设置编码
            o->ptr = (void*)((long)value);	// 设置value
        } else {
            o = createObject(OBJ_STRING,sdsfromlonglong(value));
        }
    }
    return o;
}

总结

String有三种编码方式,从client接收过来的字符串(本文只分析了简单的inline)被解析成了raw编码的robj对象,然后redis尝试转换成更加紧凑的embstr或者更节省内存的int编码。

无法对超过long long的值进行自增操作。


以上所述就是小编给大家介绍的《Redis-String类型》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

打造有吸引力的学习型社群

打造有吸引力的学习型社群

苏平、田士杰、吕守玉 / 机械工业出版社 / 45.00元

本书首先对社群的定位、准备和吸引粉丝方面等做了饶有趣味的介绍,从社群黏度的提升、社群知识的迭代与转化和社群的持续发展等多个角度入手,对学习型社群的运营手段、运营模式、运营规律和运营经验等进行了全方位剖析。从中国培训师沙龙这个公益社群近十年成功运营的经验中,为如何经营好学习型社群总结出了一套系统性的、具有实操价值的方法。并以此为基础,扩展到知识管理、团队管理、内容IP等领域,为有致于社团建设以及优质......一起来看看 《打造有吸引力的学习型社群》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具