美文网首页redis 学习
【Redis源码】Redis Set命令详解

【Redis源码】Redis Set命令详解

作者: zeekling | 来源:发表于2020-11-10 23:28 被阅读0次

    简介

    set命令用于将key-value设置到数据库。如果key已经设置,则set会用新值覆盖旧值,不管原value是何种类型,如果在设置时不指定EX或PX参数,set命令会清除原有超时时间。

    格式:

    SET key value [NX] [XX] [EX <seconds>] [PX <milliseconds>]
    

    参数:

    • NX: 当数据库中key不存在时,可以将key-value添加到数据
      库。
    • XX: 当数据库中key存在时,可以将key-value设置到数据库,
      与NX参数互斥。
    • EX: key的超时秒数。
    • PX: key的超时毫秒数,与EX参数互斥。

    命令行解析额外参数

    set命令共支持NX、XX、EX、PX这4个额外参数,在执行set命令时,需要首先对这4个参数进行解析,此时需要3个局部变量来辅助实现:

    robj *expire = NULL;
    int unit = UNIT_SECONDS;
    int flags = OBJ_SET_NO_FLAGS;
    

    expire:超时时间,robj类型。我们知道,Redis在解析命令行参数时,会将各个参数解析成robj类型,当expire值不为NULL则表示需要设置key的超时时间。

    unit:字符串的超时时间单位有秒和毫秒两种,程序中根据此值来确认超时的单位,此值只有两个取值,分别为:

    #define UNIT_SECONDS 0 //单位:秒
    #define UNIT_MILLISECONDS 1 //单位:毫秒
    

    flags:int类型,它是一个二进制串,程序中根据此值来确定key是否应该被设置到数据库。它由下列5个值来表示不同的含义:

    #define OBJ_SET_NO_FLAGS 0
    #define OBJ_SET_NX (1<<0) //标识key没有被设置过
    #define OBJ_SET_XX (1<<1) //标识key已经存在
    #define OBJ_SET_EX (1<<2) //标识key的超时时间被设置为单位秒
    #define OBJ_SET_PX (1<<3) //标识key的超时时间被设置为单位毫秒
    

    在知道了这3个变量的意义之后,再来看解析参数的具体过程。由set命令的参数格式得知,前3个参数为set、key、value,这3个参数是通用参数,我们暂时先不考虑,先从第4个参数开始依次向后通过
    for循环解析:

    for (j = 3; j < c->argc; j++) {
    char *a = c->argv[j]->ptr;
    robj *next = (j == c->argc-1) ? NULL : c->argv[j+1];
    

    a表示遍历参数时遇到的参数字符串;next表示当前遍历参数的下个参数,如果当前遍历到最后一个参数时,*next的值为NULL。

    如果遇到参数NX(不区分大小写),并且没有设置过OBJ_SET_XX,表示key在没有被设置过的情况下才可以被设置,flags赋值如下。

    flags |= OBJ_SET_NX;
    

    如果遇到参数XX(不区分大小写),并且没有设置这OBJ_SET_NX时,表示key在已经被设置的情况下才可以被设置,flags赋值如下。

    flags |= OBJ_SET_XX;
    

    如果遇到参数EX(不区分大小写),并且没有设置过OBJ_SET_PX,且下个参数存在,表示key的过期时间单位为秒,秒数由下个参数指定。

    flags |= OBJ_SET_EX;
    unit = UNIT_SECONDS;
    expire = next;
    j++;
    

    设置过期时间时,由EX和时间两个参数共同确定,所以EX的下个参数肯定为秒数值,所以直接跳过下个参数的循环,j++。

    如果遇到参数PX(不区分大小写),并且没有设置过OBJ_SET_EX,且下个参数存在。表示key的过期时间单位为毫秒,毫秒数由下个参数指定。

    flags |= OBJ_SET_PX;
    unit = UNIT_MILLISECONDS;
    expire = next;
    j++;
    

    设置过期毫秒时,由PX和时间两个参数共同确定,所以PX的下个参数肯定为毫秒值,所以直接跳到下个参数的循环,j++。

    value编码

    为了节省空间,在将key-value设置到数据库之前,根据value的不同长度和类型对value进行编码。编码的函数为:

    robj *tryObjectEncoding(robj *o)
    

    该函数执行过程经过如下几步。

    判断o的类型是否为string类型。如果不为string类型则不能对robj类型进行操作:

    serverAssertWithInfo(NULL,o,o->type == OBJ_STRING);
    

    判断o的encoding是否为sds类型,只有sds类型的数据才可以进一步优化:

    if (!sdsEncodedObject(o)) return o;
    

    sdsEncodedObject的定义如下:

    #define sdsEncodedObject(objptr) (objptr->encoding == OBJ_ENCODING_RAW || objptr->encoding == OBJ_ENCODING_EMBSTR)
    
    

    此时的encoding为OBJ_ENCODING_EMBSTR,所以此时是满足条件的。

    判断引用计数refcount,如果对象的引用计数大于1,表示此对象在多处被引用。在tryObjectEncoding函数结束时可能会修改o的值,所以贸然继续进行可能会造成其他影响,所以在refcount大于1的情况下,结束函数的运行,将o直接返回:

    if (o->refcount > 1) return o;
    

    求value的字符串长度,当长度小于等于20时,试图将value转化为long类型,如果转换成功,则分为两种情况处理:

    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;
    }
    

    其中MAXMEMORY_FLAG_NO_SHARED_INTEGERS和OBJ_SHARED_INTEGERS的定义如下:

    #define MAXMEMORY_FLAG_NO_SHARED_INTEGERS \
    (MAXMEMORY_FLAG_LRU|MAXMEMORY_FLAG_LFU)
    #define OBJ_SHARED_INTEGERS 10000
    

    第一种情况: 如果Redis的配置不要求运行LRU或LFU替换算法,并且转换后的value值小于OBJ_SHARED_INTEGERS,那么会返回共享数字对象。之所以这里的判断跟替换算法有关,是因为替换算法要求每个robj有不同的lru字段值,所以用了替换算法就不能共享robj了。通过上一章我们知道shared.integers是一个长度为10000的数组,里面预存了10000个数字对象,从0到9999。这些对象都是encoding=OBJ_ENCODING_INT的robj对象。

    第二种情况: 如果不能返回共享对象,那么将原来的robj的encoding改为OBJ_ENCODING_INT,这时robj的ptr字段直接存储为这个long型的值。robj的ptr字段本来是一个void*指针,所以在64位机器占8字节的长度,而一个long也是8字节,所以不论ptr存一个指针地址还是一个long型的值,都不会有额外的内存开销。

    对于那些不能转成64位long的字符串最后再做两步处理:

    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;
    }
    if (o->encoding == OBJ_ENCODING_RAW &&
    sdsavail(s) > len/10)
    {
    o->ptr = sdsRemoveFreeSpace(o->ptr);
    }
    
    1. 如果字符串长度小于等于OBJ_ENCODING_EMBSTR_SIZE_LIMIT,定义为44,那么调用
      createEmbeddedStringObject将encoding改为OBJ_ENCODING_EMBSTR;
    2. 如果前面所有的编码尝试都没有成功,此时仍然是OBJ_ENCODING_RAW类型,且sds里空余字节过多,那么就会调用sds的sdsRemoveFreeSpace接口来释放空余字节。通过以上5个步骤,我们来看一下set key1100现在的第2个参数的

    数据库添加key-value

    当将value值优化好之后,调用setGenericCommand函数将keyvalue设置到数据库。

    set命令调用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) ;
    
    

    此时需要根据之前所赋值的flags来确定现在是否可以将key-value设置成功。

    if ((flags & OBJ_SET_NX && lookupKeyWrite(c->db,key) != NULL) ||
    (flags & OBJ_SET_XX && lookupKeyWrite(c->db,key) == NULL))
    

    当有OBJ_SET_NX标识时,需要保证当前数据库中没有key值。当有OBJ_SET_XX时,需要保证当前数据库中已经有key值。否则直接报错退出。
    当判断当前key-value可以写入数据库之后,调用setKey方法将key-value写入数据库。

    void setKey(redisDb *db, robj *key, robj *val) {
    if (lookupKeyWrite(db,key) == NULL) {
    dbAdd(db,key,val);
    } else {
    dbOverwrite(db,key,val);
    }
    incrRefCount(val);
    removeExpire(db,key);
    ....
    }
    

    setKey方法调用dbAdd或dbOverwrite方法来写入key-value,依据当前数据库中是否有key来决定采用哪个函数来写入。数据的写入实际是将key-value写入了redisDb的dict内,字典在之前介绍过,在此不再赘述。注意在写入key-value时,不管之前这个key是否设置为超时时间,这里将该key的超时时间移除。

    设置超时时间

    将key-value设置到数据库之后,如果命令行参数里指定了超时时间,那么就需要设置key的超时时间。当然在设置超时时间之前需要判断时间值是否为long类型。Redis key的超时时间实际存储的是当前key的到期毫秒时间戳,所以在指定超时时间单位为秒时,需要将时间值乘以1000来转化为毫秒数,将当前时间加上超时毫秒数的结果就是key的超时毫秒时间戳。

    Redis将所有含有超时时间的key存储到redisDb的expire字典内,ttl命令可以快速确定key的超时秒数,就是通过查找这个字典实现的。

    通过以上4个步骤已经成功地将一个key-value设置到Redis的数据库中。


    标 题:《【Redis源码】Redis Set命令详解
    作 者:zeekling
    提 示:转载请注明文章转载自个人博客:小令童鞋

    相关文章

      网友评论

        本文标题:【Redis源码】Redis Set命令详解

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