简介
set应该是最常用的命令吧
那就来看一下,set的时候到底做了些什么
代码
最常用的set方法用到了下面的函数,所以先看下面的
int getLongLongFromObject(robj *o, long long *target)
如果o是字符串类型,调用string2ll(util.c)转为数字,如果o就是数字(OBJ_ENCODING_INT),直接取ptr
long long getExpire(redisDb *db, robj *key)
struct redisDb {
dict* expires;
}
从expires这个dict中查找key,没找到返回-1;找到,返回value
redisDb的expires存的是key的过期时间(时间戳毫秒)
int keyIsExpired(redisDb *db, robj *key)
调用getExpire,返回-1表示不过期,函数返回0
把返回值与当前时间比较,判断是否过期
redisDb的expires存的是
void propagateExpire(redisDb *db, robj *key, int lazy)
如果lazy为0,那就构建del命令;否则构建unlink命令
同步到aof和主从
int expireIfNeeded(redisDb *db, robj *key)
调用keyIsExpired,如果不过期,直接返回0
如果key过期了,server.stat_expiredkeys++,并调用propagateExpire,server.lazyfree_lazy_expire
根据server.lazyfree_lazy_expire,调用dbAsyncDelete 或 dbSyncDelete
dbSyncDelete:删除db->expires 和 db->dict
dbAsyncDelete(lazyfree.c):
dictDelete 和 dictUnlink都会把entry从数组中去除,但dictDelete会马上调用 dict->type->keyDestructor 和 dict->type->valDestructor,而dictUnlink不会,并返回entry
这个函数会del expires的,unlink dict的
然而并不是一定会异步删除,redis使用了一个数值表示redisObject的大小
redisObject是链表、hashtable、set,取其大小;其他类型都是1
lazyfree.c: static size_t lazyfree_objects = 0 记录lazy释放的总数量
只有当这个数值大于64 并且 redisObejct的refcount == 1才使用异步
异步的操作:
++ lazyfree_objects
调用bioCreateBackgroundJob(int type, void *arg1, void *arg2, void *arg3)
这个函数第一个参数type,对应bio里的3个线程类型,后面就是参数
其实就是放到队列中,等线程去执行
队列的元素结构体:
struct bio_job {
time_t time; /* Time at which the job was created. */
/* Job specific arguments pointers. If we need to pass more than three
* arguments we can just pass a pointer to a structure or alike. */
void *arg1, *arg2, *arg3;
};
这里,time就是当前时间;arg1为要释放的value指针,后面的都是NULL
除了放入队列,还会记录任务数量加一
static unsigned long long bio_pending[BIO_NUM_OPS] = {0};
这些操作都需要加锁,static pthread_mutex_t bio_mutex[BIO_NUM_OPS];
这些东西都做完了,最后释放dictEntry,结束
robj *lookupKey(redisDb *db, robj *key, int flags)
就是从db的dict中取出val
当aof、rdb子进程都没有开启,并且flags没有LOOKUP_NOTOUCH,才更新lru
void dbAdd(redisDb *db, robj *key, robj *val)
这里需要先把 key->ptr(sds) 复制一份
调用dictAdd 添加到db->dict中
void dbOverwrite(redisDb *db, robj *key, robj *val)
首先也是先从dict中找到entry,替换value
然后如果使用 LFU 模式,需要把oldValue的lru复制到 新value的lru
然后需要释放旧的value,如果不是lazy_free(默认)(这里判断的是server.lazyfree_lazy_server_del),直接释放;如果是lazy_free,和上面的逻辑差不多,也是判断robj的size
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);
signalModifiedKey(db,key);
}
把key设置到db->dict,增加val引用
removeExpire: db->expires中删除key
set命令的通用方法:
void setGenericCommand(client *c, int flags, robj *key, robj *val, robj *expire, int unit, robj *ok_reply, robj *abort_reply) {
long long milliseconds = 0; /* initialized to avoid any harmness warning */
if (expire) {
if (getLongLongFromObjectOrReply(c, expire, &milliseconds, NULL) != C_OK)
return;
if (milliseconds <= 0) {
addReplyErrorFormat(c,"invalid expire time in %s",c->cmd->name);
return;
}
if (unit == UNIT_SECONDS) milliseconds *= 1000;
}
if ((flags & OBJ_SET_NX && lookupKeyWrite(c->db,key) != NULL) ||
(flags & OBJ_SET_XX && lookupKeyWrite(c->db,key) == NULL))
{
addReply(c, abort_reply ? abort_reply : shared.nullbulk);
return;
}
setKey(c->db,key,val);
server.dirty++;
if (expire) setExpire(c,c->db,key,mstime()+milliseconds);
notifyKeyspaceEvent(NOTIFY_STRING,"set",key,c->db->id);
if (expire) notifyKeyspaceEvent(NOTIFY_GENERIC,
"expire",key,c->db->id);
addReply(c, ok_reply ? ok_reply : shared.ok);
}
expire是过期时间值,unit有秒和毫秒
如果set命令有nx 或者 xx,需要先查找有没有这个key,如果不需要更新,直接return
setKey函数如上,就是添加到db->dict
server.dirty加一,表示有更新;这个在aof同步中有用到
setExpire就是添加到 db->expires,db->expires的value为过期时间戳
前面铺垫了这么多,终于来到setCommand函数
typedef struct client {
int argc; /* Num of arguments of current command. */
robj **argv; /* Arguments of current command. */
}
set完整命令:
SET key value [NX] [XX] [EX ] [PX ]
client中的argc和argv为客户端发送的命令
其实就是解析一下参数,调用setGenericCommand
set函数基本上就是调用setGenericCommand
Get
robj *lookupKeyReadOrReply(client *c, robj *key, robj *reply)
lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk); nullbulk为 "$-1\r\n"
第二层:lookupKeyRead(c->db, key)
第三层:lookupKeyReadWithFlags(db,key,LOOKUP_NONE)
robj *lookupKeyReadWithFlags(redisDb *db, robj *key, int flags)
这个函数里面用到了lookupKey方法,就是取db.dict
取完之后,统计命中和未命中
redisServer {
long long stat_keyspace_hits; /* Number of successful lookups of keys */
long long stat_keyspace_misses; /* Number of failed lookups of keys */
}
取之前,需要先调用expireIfNeeded,如果有过期的,先过期掉
lookupKeyReadWithFlags结束
那第二层的lookupKeyRead,就是调用lookupKeyReadWithFlags
lookupKeyReadOrReply:
调用lookupKeyRead,如果找到了就return;如果没找到,调用addReply(c, shared.nullbulk)
现在可以来看getCommand
int getGenericCommand(client *c) {
robj *o;
//先找
if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk)) == NULL)
//没找到时,上面已经调用了addReply,所以这里不需要再回复
return C_OK;
//如果找出来不是string类型,报错
//这里可以看出,get命令只能用于字符串类型
if (o->type != OBJ_STRING) {
addReply(c,shared.wrongtypeerr);
return C_ERR;
} else {
addReplyBulk(c,o);
return C_OK;
}
}
addReplyBulk这个来看一下
/* Add a Redis Object as a bulk reply */
void addReplyBulk(client *c, robj *obj) {
addReplyBulkLen(c,obj);
addReply(c,obj);
addReply(c,shared.crlf);
}
redis字符串返回格式:
$[长度]
[内容]
所以需要先输出长度
输出长度的函数:void addReplyBulkLen(client *c, robj *obj)
robj可能是EMBSTR、RAW、INT,前2种调用sdslen就能获取长度,如果是int,那就用除10获取长度
获得长度之后,要把长度变成字符串
当然,redis把32以内(不包括32)的长度的字符串做成了 常用池
#define OBJ_SHARED_BULKHDR_LEN 32
struct sharedObjectsStruct {
robj *bulkhdr[OBJ_SHARED_BULKHDR_LEN]
}
这个数组已经构造好了 $[n]\r\n
那大于等于32的,就现场构造:
直接创建128长度的char数组(自信足够长),调用util.c的ll2string填充进数组,最后补上\r\n
输出完长度,就可以调用
addReply(c,obj);
addReply(c,shared.crlf);
addReplyBulk结束
get总结来说就是,看有没有过期,查找,返回
del
del命令可以带多个key
void delGenericCommand(client *c, int lazy)
对于delCommand,lazy为0
遍历key,首先调用expireIfNeeded,然后根据lazy调用dbSyncDelete或dbAsyncDelete
这2个函数返回1表示存在这个Key,0表示不存在
根据这个返回,统计删除的数量,作为返回结果;并且,如果返回1,增加server.dirty
返回给客户端使用addReplyLongLong(client *c, long long ll)函数,返回样式为
:[num]
这个是redis返回数值的格式
unlink
和del类似,也是调用delGenericCommand,lazy为1
也就是说调用了dbAsyncDelete
incr/decr
expireIfNeeded
如果key不存在或者Key没过期,返回0;否则返回1 (对于slave节点,返回1,但是不会删除,需要等待master节点的同步)
lookupKeyRead 和 lookupKeyWrite:
当key不存在或者没过期时,都是调用lookupKey
lookupKeyWrite就是调用 expireIfNeeded 和 lookupKey
lookupKeyRead调用lookupKeyReadWithFlags(db,key,LOOKUP_NONE),这个函数也是先调用expireIfNeeded
分歧就在expireIfNeeded返回1,如果是slave节点,直接返回NULL;因为slave节点不会删除,不能调用lookupKey
void incrDecrCommand(client *c, long long incr)
首先调用lookupKeyWrite,如果不为NULL并且类型不是string,返回类型错误
然后调用getLongLongFromObjectOrReply,转型为long long,如果为NULL,则为0
转完就加上incr
加完之后,可以直接修改原来的对象,但是是有条件的
条件:引用数量为1,encoding为INT,不是shared对象,在LONG_MIN和LONG_MAX 之间
不满足的话,就要使用robj *createStringObjectFromLongLongForValue(long long value)
创建的对象,如果为LONG_MIN和LONG_MAX 之间,则encoding直接为INT,否则为字符串
最后修改到db->dict,返回数值结果 :[num]
incrByFloat
使用了C函数 strtold <stdlib.h>
long double strtold( const char *restrict str, char **restrict str_end );
(C99 起)
函数会舍弃任何空白符(由 std::isspace() 确定),直至找到首个非空白符。然后它会取用尽可能多的字符,以构成合法的浮点数表示,并将它们转换成浮点值。
第一个参数就是要转换的字符串
第二个参数的解释:函数设置 str_end 所指向的指针指向最后被转译字符的后一字符,若 str_end 为 NULL ,则忽略它。
如果转换正常,第二个参数应该是指向第一个字符串的结束符
redis是这样判断转换是否成功:
第二个参数指针减去第一个字符指针,应该等于第一个字符的长度,不等于就是没全部用上,转换失败
errno == ERANGE 并且 返回值为HUGE_VAL或-HUGE_VAL或0,超出了范围
转换出来的值是NAN
incrByFloat和前面的incr是差不多的,都是lookupKeyWrite,然后把原值和参数转long double,相加
但是回存有区别,因为这里是小数类型
相加完都是要创建一个新的字符串
转换为字符串使用snprintf函数,数组初始化大小 5 * 1024,还用了int humanfriendly标志位做区分
如果humanfriendly,格式为%.17Lf,小数点后只保留17位,并且把末尾的0去掉
如果不是humanfriendly,格式为%.17Lg,没有其他处理
可以看出,都是只保留小数17位
%f 小数形式
%E 科学计数法形式
%g 取%E %f中,较短的那个
redis里面,小数的存储都是string的,要用的时候再转
重写命令:
为了保证同步(AOF)的数据精度,把命令改为set XXX 最终值