【Redis源码】Redis Set命令详解

a3c7e433ab54bcf9f65b4042a058cace.jpg

简介

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源码】Redis Set命令详解
作 者:zeekling
提 示:转载请注明文章转载自个人博客:浪浪山旁那个村

评论

取消