简介

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 最终值