redis源码分析与思考(十三)——字符串类型的命令实现(t_string.c)

版权声明:博主GitHub地址https://github.com/suyeq欢迎大家前来交流学习 https://blog.csdn.net/hackersuye/article/details/83107676

    在对字符串操作的命令中,主要有增加删查该、批处理操作以及编码的转换命令,现在列出对字符串对象操作的主要常用命令:

常用命令表

命令 对应操作 时间复杂度
set key value 增加键值对 O(1)
setex key seconds value 增加键值对,且设置秒级的过期时间 O(1)
psetex key milliseconds value 增加键值对,且设置毫秒级的过期时间 O(1)
setnx key value 增加键值对,键不存在才能成功 O(1)
set key value xx 修改键值,键必须存在 O(1)
mset key value [key value…] 批量设置键 O(n)
mget key [key…] 批量获得键值 O(n)
get key 获得键值 O(1)
del key 删除键 O(1)
incr key 键值自增 O(1)
decr key 键值自减 O(1)
incrby key increment 自增指定数字 O(1)
decrby key decrement 自减指定数字 O(1)
incrybyfloat key increment 自增浮点数 O(1)
append key value 追加值 O(1)
strlen key 获取键值的长度 O(1)
setrange key offset value 设置指定位置的字符 O(1)
getrange key start end 获取指定偏移量的键值 O(n)

    在每次增加键的时候,都会检查其大小是否超过了512M:

static int checkStringLength(redisClient *c, long long size) {
    if (size > 512*1024*1024) {
    //返回客户端错误信息
        addReplyError(c,"string exceeds maximum allowed size (512MB)");
        return REDIS_ERR;
    }
    return REDIS_OK;
}

键的增加

    键增加的策略是先检查是否超了大小,再检查是否有生存时间,最后检查输入命令的参数,下面列出SET命令的底层实现:

#define REDIS_SET_NO_FLAGS 0
#define REDIS_SET_NX (1<<0)     /* Set if key not exists. */
#define REDIS_SET_XX (1<<1)     /* Set if key exists. */

void setGenericCommand(redisClient *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) {
        // 将取 expire 参数的值取出来
        if (getLongLongFromObjectOrReply(c, expire, &milliseconds, NULL) != REDIS_OK)
            return;
        // expire 参数的值不正确时报错
        if (milliseconds <= 0) {
            addReplyError(c,"invalid expire time in SETEX");
            return;
        }
        // 不论输入的过期时间是秒还是毫秒
        // Redis 实际都以毫秒的形式保存过期时间
        // 如果输入的过期时间为秒,那么将它转换为毫秒
        if (unit == UNIT_SECONDS) milliseconds *= 1000;
    }
    // 如果设置了 NX 或者 XX 参数,那么检查条件是否不符合这两个设置
    // 在条件不符合时报错,报错的内容由 abort_reply 参数决定
    if ((flags & REDIS_SET_NX && lookupKeyWrite(c->db,key) != NULL) ||
        (flags & REDIS_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->db,key,mstime()+milliseconds);
    // 发送事件通知,通知键的增加情况
    notifyKeyspaceEvent(REDIS_NOTIFY_STRING,"set",key,c->db->id);
    // 发送事件通知
    if (expire) notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,
        "expire",key,c->db->id);
    // 设置成功,向客户端发送回复
    // 回复的内容由 ok_reply 决定
    addReply(c, ok_reply ? ok_reply : shared.ok);
}

    SET命令的实现,先判断命令参数是哪个,再调用setGenericCommand函数:

void setCommand(redisClient *c) {
    int j;
    robj *expire = NULL;
    int unit = UNIT_SECONDS;
    int flags = REDIS_SET_NO_FLAGS;
    // 判断选项参数
    for (j = 3; j < c->argc; j++) {
        char *a = c->argv[j]->ptr;
        robj *next = (j == c->argc-1) ? NULL : c->argv[j+1];
        if ((a[0] == 'n' || a[0] == 'N') &&
            (a[1] == 'x' || a[1] == 'X') && a[2] == '\0') {
            flags |= REDIS_SET_NX;
        } else if ((a[0] == 'x' || a[0] == 'X') &&
                   (a[1] == 'x' || a[1] == 'X') && a[2] == '\0') {
            flags |= REDIS_SET_XX;
        } else if ((a[0] == 'e' || a[0] == 'E') &&
                   (a[1] == 'x' || a[1] == 'X') && a[2] == '\0' && next) {
            unit = UNIT_SECONDS;
            expire = next;
            j++;
        } else if ((a[0] == 'p' || a[0] == 'P') &&
                   (a[1] == 'x' || a[1] == 'X') && a[2] == '\0' && next) {
            unit = UNIT_MILLISECONDS;
            expire = next;
            j++;
        } else {
            addReply(c,shared.syntaxerr);
            return;
        }
    }
    // 尝试对值对象进行编码,如果是raw与embstr字符串就尝试将其装换为int存贮
    c->argv[2] = tryObjectEncoding(c->argv[2]);
    setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);
}

    tryObjectEncoding函数的作用是对字符串对象进行重新编码,看是否能将其装换为int编码存贮,raw是否能转换为emstr格式,需要对象是raw与embstr格式字符串时,这样可以节约内存:

robj *tryObjectEncoding(robj *o) {
    long value;
    sds s = o->ptr;
    size_t len;
    redisAssertWithInfo(NULL,o,o->type == REDIS_STRING)// 只在字符串的编码为 RAW 或者 EMBSTR 时尝试进行编码
    if (!sdsEncodedObject(o)) return o;
     // 不对共享对象进行编码
     if (o->refcount > 1) return o;
    // 对字符串进行检查
    // 只对长度小于或等于 21 字节,并且可以被解释为整数的字符串进行编码
    len = sdslen(s);
    if (len <= 21 && string2l(s,len,&value)) {
        if (server.maxmemory == 0 &&
            value >= 0 &&
            value < REDIS_SHARED_INTEGERS)
        {
            decrRefCount(o);
            incrRefCount(shared.integers[value]);
            return shared.integers[value];
        } else {
            if (o->encoding == REDIS_ENCODING_RAW) sdsfree(o->ptr);
            o->encoding = REDIS_ENCODING_INT;
            o->ptr = (void*) value;
            return o;
        }
    }
    // 尝试将 RAW 编码的字符串编码为 EMBSTR 编码
    if (len <= REDIS_ENCODING_EMBSTR_SIZE_LIMIT) {
        robj *emb;
        if (o->encoding == REDIS_ENCODING_EMBSTR) return o;
        emb = createEmbeddedStringObject(s,sdslen(s));
        decrRefCount(o);
        return emb;
    }
    // 这个对象没办法进行编码,尝试从 SDS 中移除所有空余空间
    if (o->encoding == REDIS_ENCODING_RAW &&
        sdsavail(s) > len/10)
    {
        o->ptr = sdsRemoveFreeSpace(o->ptr);
    }
    /* Return the original object. */
    return o;
}

    其余SET命令:

//SETNX命令
void setnxCommand(redisClient *c) {
    c->argv[2] = tryObjectEncoding(c->argv[2]);
    setGenericCommand(c,REDIS_SET_NX,c->argv[1],c->argv[2],NULL,0,shared.cone,shared.czero);
}

//SETEX命令
void setexCommand(redisClient *c) {
    c->argv[3] = tryObjectEncoding(c->argv[3]);
    setGenericCommand(c,REDIS_SET_NO_FLAGS,c->argv[1],c->argv[3],c->argv[2],UNIT_SECONDS,NULL,NULL);
}

//PSETEX命令
void psetexCommand(redisClient *c) {
    c->argv[3] = tryObjectEncoding(c->argv[3]);
    setGenericCommand(c,REDIS_SET_NO_FLAGS,c->argv[1],c->argv[3],c->argv[2],UNIT_MILLISECONDS,NULL,NULL);
}

获取键

    键的获取比较的简单,在这里直接贴出源代码以及注释:

//获取键的底层实现
int getGenericCommand(redisClient *c) {
    robj *o;
    // 尝试从数据库中取出键 c->argv[1] 对应的值对象
    // 如果键不存在时,向客户端发送回复信息,并返回 NULL
    //尝试取出键值
    if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk)) == NULL)
        return REDIS_OK;
    // 值对象存在,检查它的类型
    if (o->type != REDIS_STRING) {
        // 类型错误
        addReply(c,shared.wrongtypeerr);
        return REDIS_ERR;
    } else {
        // 类型正确,向客户端返回对象的值
        addReplyBulk(c,o);
        return REDIS_OK;
    }
}

//GET命令
void getCommand(redisClient *c) {
    getGenericCommand(c);
}

//GETSET命令,先返回值,再修改
void getsetCommand(redisClient *c) {
    // 取出并返回键的值对象
    if (getGenericCommand(c) == REDIS_ERR) return;
    // 编码键的新值 c->argv[2]
    c->argv[2] = tryObjectEncoding(c->argv[2]);
    // 将数据库中关联键 c->argv[1] 和新值对象 c->argv[2]
    setKey(c->db,c->argv[1],c->argv[2]);
    // 发送事件通知
    notifyKeyspaceEvent(REDIS_NOTIFY_STRING,"set",c->argv[1],c->db->id);
    // 将服务器设为脏
    server.dirty++;
}

批处理操作

    既然有了SET与GET命令,那么为什么要加入批处理操作呢?其实主要是解决网络延迟的问题,假如你有10万条数据需要插入到数据库中,每条插入数据的命令从客户端传送到服务端的延迟是1ms,那么也要花费100s的时间来传输。批处理的策略是将所有的命令通过一次传输到服务端,将网络延迟的影响降到了最少:

//MSET命令底层实现
void msetGenericCommand(redisClient *c, int nx) {
    int j, busykeys = 0;
    //检查格式是否正确
    // 键值参数不是成相成对出现的,格式不正确
    if ((c->argc % 2) == 0) {
        //返回给客户端错误信息
        addReplyError(c,"wrong number of arguments for MSET");
        return;
    }
    /* Handle the NX flag. The MSETNX semantic is to return zero and don't
     * set nothing at all if at least one already key exists. */
    // 如果 nx 参数为真,那么检查所有输入键在数据库中是否存在
    // 只要有一个键是存在的,那么就向客户端发送空回复
    // 并放弃执行接下来的设置操作,保证了原子性
    if (nx) {
    //判断其中是否有有键存在
        for (j = 1; j < c->argc; j += 2) {
            if (lookupKeyWrite(c->db,c->argv[j]) != NULL) {
                busykeys++;
            }
        }
        // 键存在
        // 发送空白回复,并放弃执行接下来的设置操作
        if (busykeys) {
            addReply(c, shared.czero);
            return;
        }
    }
    // 设置所有键值对
    for (j = 1; j < c->argc; j += 2) {
        // 对值对象进行解码
        c->argv[j+1] = tryObjectEncoding(c->argv[j+1]);
        // 将键值对关联到数据库
        // c->argc[j] 为键
        // c->argc[j+1] 为值
        setKey(c->db,c->argv[j],c->argv[j+1]);
        // 发送事件通知
        notifyKeyspaceEvent(REDIS_NOTIFY_STRING,"set",c->argv[j],c->db->id);
    }
    // 将服务器设为脏
    server.dirty += (c->argc-1)/2;
    // 设置成功
    // MSET 返回 OK ,而 MSETNX 返回 1
    addReply(c, nx ? shared.cone : shared.ok);
}
//MSET命令
void msetCommand(redisClient *c) {
    msetGenericCommand(c,0);
}
//MSETNX命令
void msetnxCommand(redisClient *c) {
    msetGenericCommand(c,1);
}

    MGET命令的实现:

void mgetCommand(redisClient *c) {
    int j;
    addReplyMultiBulkLen(c,c->argc-1);
    // 查找并返回所有输入键的值
    for (j = 1; j < c->argc; j++) {
        // 查找键 c->argc[j] 的值
        robj *o = lookupKeyRead(c->db,c->argv[j]);
        if (o == NULL) {
            // 值不存在,向客户端发送空回复
            addReply(c,shared.nullbulk);
        } else {
            if (o->type != REDIS_STRING) {
                // 值存在,但不是字符串类型
                addReply(c,shared.nullbulk);
            } else {
                // 值存在,并且是字符串
                addReplyBulk(c,o);
            }
        }
    }
}

修改以及获取值对象操作

    SETRANGE命令是修改值对象中指定字符,而GETRANGE是获取部分字符串,也就是值对象:

//SETRANGE命令
void setrangeCommand(redisClient *c) {
    robj *o;
    long offset;
    sds value = c->argv[3]->ptr;
    // 取出 offset 参数
    if (getLongFromObjectOrReply(c,c->argv[2],&offset,NULL) != REDIS_OK)
        return;
    // 检查 offset 参数
    if (offset < 0) {
        addReplyError(c,"offset is out of range");
        return;
    }
    // 取出键现在的值对象
    o = lookupKeyWrite(c->db,c->argv[1]);
    if (o == NULL) {
        // 键不存在于数据库中。。。
        // value 为空,没有什么可设置的,向客户端返回 0
        if (sdslen(value) == 0) {
            addReply(c,shared.czero);
            return;
        }
        // 如果设置后的长度会超过 Redis 的限制的话
        // 那么放弃设置,向客户端发送一个出错回复
        if (checkStringLength(c,offset+sdslen(value)) != REDIS_OK)
            return;
        // 如果 value 没有问题,可以设置,那么创建一个空字符串值对象
        // 并在数据库中关联键 c->argv[1] 和这个空字符串对象
        o = createObject(REDIS_STRING,sdsempty());
        dbAdd(c->db,c->argv[1],o);
    } else {
        size_t olen;
        // 值对象存在。。。
        // 检查值对象的类型
        if (checkType(c,o,REDIS_STRING))
            return;
        /* Return existing string length when setting nothing */
        // 取出原有字符串的长度
        olen = stringObjectLen(o);
        // value 为空,没有什么可设置的,向客户端返回 0
        if (sdslen(value) == 0) {
            addReplyLongLong(c,olen);
            return;
        }
        /* Return when the resulting string exceeds allowed size */
        // 如果设置后的长度会超过 Redis 的限制的话
        // 那么放弃设置,向客户端发送一个出错回复
        if (checkStringLength(c,offset+sdslen(value)) != REDIS_OK)
            return;
        /* Create a copy when the object is shared or encoded. */
        o = dbUnshareStringValue(c->db,c->argv[1],o);
    }
    // 这里的 sdslen(value) > 0 其实可以去掉
    // 前面已经做了检测了
    if (sdslen(value) > 0) {
        // 扩展字符串值对象,并将值对象指向它
        o->ptr = sdsgrowzero(o->ptr,offset+sdslen(value));
        // 将 value 复制到字符串中的指定的位置
        memcpy((char*)o->ptr+offset,value,sdslen(value));
        // 向数据库发送键被修改的信号
        signalModifiedKey(c->db,c->argv[1]);
        // 发送事件通知
        notifyKeyspaceEvent(REDIS_NOTIFY_STRING,
            "setrange",c->argv[1],c->db->id);
        // 将服务器设为脏
        server.dirty++;
    }
    // 设置成功,返回新的字符串值给客户端
    addReplyLongLong(c,sdslen(o->ptr));
}

//GETRANGE命令,start与end决定返回值对象的范围
void getrangeCommand(redisClient *c) {
    robj *o;
    long start, end;
    char *str, llbuf[32];
    size_t strlen;
    // 取出 start 参数
    if (getLongFromObjectOrReply(c,c->argv[2],&start,NULL) != REDIS_OK)
        return;
    // 取出 end 参数
    if (getLongFromObjectOrReply(c,c->argv[3],&end,NULL) != REDIS_OK)
        return;
    // 从数据库中查找键 c->argv[1] 
    if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.emptybulk)) == NULL ||
        checkType(c,o,REDIS_STRING)) return;
    // 根据编码,对对象的值进行处理
    if (o->encoding == REDIS_ENCODING_INT) {
        str = llbuf;
        strlen = ll2string(llbuf,sizeof(llbuf),(long)o->ptr);
    } else {
        str = o->ptr;
        strlen = sdslen(str);
    }
    /* Convert negative indexes */
    // 将负数索引转换为整数索引,因为start=0,end=-1表示返回所有
    if (start < 0) start = strlen+start;
    if (end < 0) end = strlen+end;
    if (start < 0) start = 0;
    if (end < 0) end = 0;
    if ((unsigned)end >= strlen) end = strlen-1;
    if (start > end) {
        // 处理索引范围为空的情况
        addReply(c,shared.emptybulk);
    } else {
        // 向客户端返回给定范围内的字符串内容
        addReplyBulkCBuffer(c,(char*)str+start,end-start+1);
    }
}

自增与自减

    值的自增与自减的策略分为三种情况,第一,值不是整数的情况下返回错误的结果。第二,值是整数的情况下返回自增或自减后的结果。第三,键不存在的情况下,增加一个键值对,按照值0自增或自减,返回结果1,其底层实现如下,浮点数也是采取相似的策略,在这就不描述了:

//incr为1,代表自增,-1代表自减
void incrDecrCommand(redisClient *c, long long incr) {
    long long value, oldvalue;
    robj *o, *new;
    // 取出值对象
    o = lookupKeyWrite(c->db,c->argv[1]);
    // 检查对象是否存在,以及类型是否正确
    if (o != NULL && checkType(c,o,REDIS_STRING)) return;
    // 取出对象的整数值,并保存到 value 参数中
    if (getLongLongFromObjectOrReply(c,o,&value,NULL) != REDIS_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;
    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(REDIS_NOTIFY_STRING,"incrby",c->argv[1],c->db->id);
    // 将服务器设为脏
    server.dirty++;
    // 返回回复
    addReply(c,shared.colon);
    addReply(c,new);
    addReply(c,shared.crlf);
}

值的追加操作

    值的追加操作涉及到编码的转换,如果是整型得到,它会将其转变为raw编码的字符串:

//APPEND命令
void appendCommand(redisClient *c) {
    size_t totlen;
    robj *o, *append;
    // 取出键相应的值对象
    o = lookupKeyWrite(c->db,c->argv[1]);
    if (o == NULL) {
        // 键值对不存在。。。
        /* Create the key */
        // 键值对不存在,创建一个新的
        c->argv[2] = tryObjectEncoding(c->argv[2]);
        dbAdd(c->db,c->argv[1],c->argv[2]);
        incrRefCount(c->argv[2]);
        totlen = stringObjectLen(c->argv[2]);
    } else {
        // 键值对存在。。。
        /* Key exists, check type */
        // 检查类型
        if (checkType(c,o,REDIS_STRING))
            return;
        /* "append" is an argument, so always an sds */
        // 检查追加操作之后,字符串的长度是否符合 Redis 的限制
        append = c->argv[2];
        totlen = stringObjectLen(o)+sdslen(append->ptr);
        //这里涉及到编码的转换
        if (checkStringLength(c,totlen) != REDIS_OK)
            return;
        /* Append the value */
        // 执行追加操作
        o = dbUnshareStringValue(c->db,c->argv[1],o);
        o->ptr = sdscatlen(o->ptr,append->ptr,sdslen(append->ptr));
        totlen = sdslen(o->ptr);
    }
    // 向数据库发送键被修改的信号
    signalModifiedKey(c->db,c->argv[1]);
    // 发送事件通知
    notifyKeyspaceEvent(REDIS_NOTIFY_STRING,"append",c->argv[1],c->db->id);
    // 将服务器设为脏
    server.dirty++;
    // 发送回复
    addReplyLongLong(c,totlen);
}

    获取键的数量:

void strlenCommand(redisClient *c) {
    robj *o;
    // 取出值对象,并进行类型检查
    if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.czero)) == NULL ||
        checkType(c,o,REDIS_STRING)) return;
    // 返回字符串值的长度
    addReplyLongLong(c,stringObjectLen(o));
}

猜你喜欢

转载自blog.csdn.net/hackersuye/article/details/83107676