《redis设计与实现》-9数据库命令实现

一 序

  上一篇按照书上的第9章的内容整理了redis数据库, 主要侧重于键值的过期及删除策略。限于偏于本篇整理数据的命令。

数据库管理的命令如下表所示:

命令描述
FLUSHDB清空当前数据库的所有key
FLUSHALL清空整个Redis服务器的所有key
DBSIZE返回当前数据库的key的个数
DEL key [key …]删除一个或多个键
EXISTS key检查给定key是否存在
SELECT id切换到指定的数据库
RANDOMKEY从当前数据库中随机返回(不删除)一个 key 。
KEYS pattern查找所有符合给定模式pattern的key
SCAN cursor [MATCH pattern] [COUNT count]增量式迭代当前数据库键
LASTSAVE返回最近一次成功将数据保存到磁盘上的时间,以 UNIX 时间戳格式表示。
TYPE key返回指定键的对象类型
SHUTDOWN停止所有客户端,关闭 redis 服务器(server)
RENAME key newkey重命名指定的key,newkey存在时覆盖
RENAMENX key newkey重命名指定的key,当且仅当newkey不存在时操作
MOVE key db移动key到指定数据库
EXPIREAT key timestamp为 key 设置生存时间,EXPIREAT 命令接受的时间参数是 UNIX 时间戳
EXPIRE key seconds以秒为单位设置 key 的生存时间
PEXPIRE key milliseconds以毫秒为单位设置 key 的生存时间
PEXPIREAT key milliseconds-timestamp以毫秒为单位设置 key 的过期 unix 时间戳
TTL key以秒为单位返回 key 的剩余生存时间
PTTL key以毫秒为单位返回 key 的剩余生存时间

二  move 

上一篇整理过超时相关,所以从move开始看。

MOVE key db

将当前数据库的 key 移动到给定的数据库 db 当中。如果当前数据库(源数据库)和给定数据库(目标数据库)有相同名字的给定 key ,或者 key 不存在于当前数据库,那么 MOVE 没有任何效果。源码在db.c

void moveCommand(client *c) {
    robj *o;
    redisDb *src, *dst;
    int srcid;
    long long dbid, expire;
    //集群模式禁用move
    if (server.cluster_enabled) {
        addReplyError(c,"MOVE is not allowed in cluster mode");
        return;
    }

    /* Obtain source and target DB pointers */
    src = c->db; // 源数据库
    srcid = c->db->id;   // 源数据库的 id
	  // 切换到目标数据库
    if (getLongLongFromObject(c->argv[2],&dbid) == C_ERR ||
        dbid < INT_MIN || dbid > INT_MAX ||
        selectDb(c,dbid) == C_ERR)
    {
        addReply(c,shared.outofrangeerr);
        return;
    }
    dst = c->db;// 目标数据库
      // 切换回源数据库
    selectDb(c,srcid); /* Back to the source DB */

    /* If the user is moving using as target the same
     * DB as the source DB it is probably an error. */
      // 如果源数据库和目标数据库相等,那么返回错误
    if (src == dst) {
        addReply(c,shared.sameobjecterr);
        return;
    }

    /* Check if the element exists and get a reference */
     // 取出要移动的对象
    o = lookupKeyWrite(c->db,c->argv[1]);
    if (!o) {
        addReply(c,shared.czero);
        return;
    }
    expire = getExpire(c->db,c->argv[1]);

    /* Return zero if the key already exists in the target DB */
      // 如果键已经存在于目标数据库,那么返回
    if (lookupKeyWrite(dst,c->argv[1]) != NULL) {
        addReply(c,shared.czero);
        return;
    }
     // 将键添加到目标数据库中
    dbAdd(dst,c->argv[1],o);
    //设置过期时间
    if (expire != -1) setExpire(dst,c->argv[1],expire);
    // 增加对对象的引用计数,避免接下来在源数据库中删除时 o 被清理
    incrRefCount(o);

    /* OK! key moved, free the entry in the source DB */
    // 将键从源数据库中返回
    dbDelete(src,c->argv[1]);
    server.dirty++;//设脏
    addReply(c,shared.cone);
}

三 rename

RENAME key newkey

将 key 改名为 newkey 。

当 key 和 newkey 相同,或者 key 不存在时,返回一个错误。

当 newkey 已经存在时, RENAME 命令将覆盖旧值。

RENAMENX key newkey

当且仅当 newkey 不存在时,将 key 改名为 newkey 。

当 key 不存在时,返回一个错误。

void renameCommand(client *c) {
    renameGenericCommand(c,0);
}

void renamenxCommand(client *c) {
    renameGenericCommand(c,1);
}

底层的函数是一样的,入参不同

void renameGenericCommand(client *c, int nx) {
    robj *o;
    long long expire;
    int samekey = 0;

    /* When source and dest key is the same, no operation is performed,
     * if the key exists, however we still return an error on unexisting key. */
      // key和newkey相同的话,设置samekey标志
    if (sdscmp(c->argv[1]->ptr,c->argv[2]->ptr) == 0) samekey = 1;
        // 以写操作读取key的值对象
    if ((o = lookupKeyWriteOrReply(c,c->argv[1],shared.nokeyerr)) == NULL)
        return;
	   // 如果key和newkey相同,nx为1发送0,否则为ok,就是结合入参来看
    if (samekey) {
        addReply(c,nx ? shared.czero : shared.ok);
        return;
    }
    // 增加引用计数,因为后面目标键也会引用这个对象
    // 如果不增加的话,当来源键被删除时,这个值对象也会被删除
    incrRefCount(o);
      // 取出来源键的过期时间,将来作为newkey的过期时间
    expire = getExpire(c->db,c->argv[1]);
    // 判断newkey的值对象是否存在
    if (lookupKeyWrite(c->db,c->argv[2]) != NULL) {
    	   // 设置nx标志,RENAMENX 则不符合已存在的条件,发送0
        if (nx) {
            decrRefCount(o);
            addReply(c,shared.czero);
            return;
        }
        /* Overwrite: delete the old key before creating the new one
         * with the same name. */
         // 如果执行的是 RENAME ,那么删除已有的目标键
        dbDelete(c->db,c->argv[2]);
    }
    // 将newkey和key的值对象关联
    dbAdd(c->db,c->argv[2],o);
     // 如果有过期时间,那么为目标键设置过期时间
    if (expire != -1) setExpire(c->db,c->argv[2],expire);
     // 删除来源键
    dbDelete(c->db,c->argv[1]);
     // 发送这两个键被修改的信号
    signalModifiedKey(c->db,c->argv[1]);
    signalModifiedKey(c->db,c->argv[2]);
    // 发送不同命令的事件通知
    notifyKeyspaceEvent(NOTIFY_GENERIC,"rename_from",
        c->argv[1],c->db->id);
    notifyKeyspaceEvent(NOTIFY_GENERIC,"rename_to",
        c->argv[2],c->db->id);
    server.dirty++;//设脏
    addReply(c,nx ? shared.cone : shared.ok);
}

四  SCAN

SCAN 命令及其相关的 SSCAN 命令、 HSCAN 命令和 ZSCAN 命令都用于增量地迭代(incrementally iterate)一集元素(a collection of elements):

  • SCAN 命令用于迭代当前数据库中的数据库键。
  • SSCAN 命令用于迭代集合键中的元素。
  • HSCAN 命令用于迭代哈希键中的键值对。
  • ZSCAN 命令用于迭代有序集合中的元素(包括元素成员和元素分值)。

以上列出的四个命令都支持增量式迭代, 它们每次执行都只会返回少量元素, 所以这些命令可以用于生产环境, 而不会出现像 KEYS 命令、 SMEMBERS 命令带来的问题 —— 当 KEYS 命令被用于处理一个大的数据库时, 又或者 SMEMBERS 命令被用于处理一个大的集合键时, 它们可能会阻塞服务器达数秒之久。

增量式迭代命令缺点: 因为在对键进行增量式迭代的过程中, 键可能会被修改, 所以增量式迭代命令只能对被返回的元素提供有限的保证 (offer limited guarantees about the returned elements)。

看下源码:

/* The SCAN command completely relies on scanGenericCommand. */
void scanCommand(client *c) {
    unsigned long cursor;
    if (parseScanCursorOrReply(c,c->argv[1],&cursor) == C_ERR) return;
    scanGenericCommand(c,NULL,cursor);
}

/* This command implements SCAN, HSCAN and SSCAN commands.
 * If object 'o' is passed, then it must be a Hash or Set object, otherwise
 * if 'o' is NULL the command will operate on the dictionary associated with
 * the current database.
 *
 * When 'o' is not NULL the function assumes that the first argument in
 * the client arguments vector is a key so it skips it before iterating
 * in order to parse options.
 *
 * In the case of a Hash object the function returns both the field and value
 * of every element on the Hash. */
 // SCAN cursor [MATCH pattern] [COUNT count]
// SCAN、HSCAN、SSCAN、ZSCAN一类命令底层实现
// o对象必须是哈希对象或集合对象,否则命令将操作当前数据库
// 如果o不是NULL,那么说明他是一个哈希或集合对象,函数将跳过这些键对象,对参数进行分析
// 如果是哈希对象,返回返回的是键值对
void scanGenericCommand(client *c, robj *o, unsigned long cursor) {
    int i, j;
    list *keys = listCreate(); //创建一个列表
    listNode *node, *nextnode;
    long count = 10;
    sds pat = NULL;
    int patlen = 0, use_pattern = 0;
    dict *ht;

    /* Object must be NULL (to iterate keys names), or the type of the object
     * must be Set, Sorted Set, or Hash. */
       // 输入类型检查
    serverAssert(o == NULL || o->type == OBJ_SET || o->type == OBJ_HASH ||
                o->type == OBJ_ZSET);

    /* Set i to the first option argument. The previous one is the cursor. */
    // 设置第一个选项参数的索引位置
    // 0    1      2      3  
    // SCAN OPTION <op_arg>         SCAN 命令的选项值从索引 2 开始
    // HSCAN <key> OPTION <op_arg>  而其他 *SCAN 命令的选项值从索引 3 开始
    i = (o == NULL) ? 2 : 3; /* Skip the key argument if needed. */

    /* Step 1: Parse options. */
       // 1. 解析选项参数
    while (i < c->argc) {
        j = c->argc - i;
        // 设定COUNT参数,COUNT 选项的作用就是让用户告知迭代命令, 在每次迭代中应该返回多少元素。
        if (!strcasecmp(c->argv[i]->ptr, "count") && j >= 2) {
        	   //保存个数到count
            if (getLongFromObjectOrReply(c, c->argv[i+1], &count, NULL)
                != C_OK)
            {
                goto cleanup;
            }
            // 如果个数小于1,语法错误
            if (count < 1) {
                addReply(c,shared.syntaxerr);
                goto cleanup;
            }

            i += 2;//参数跳过两个已经解析过的
          //匹配match,让命令只返回和给定模式相匹配的元素。
        } else if (!strcasecmp(c->argv[i]->ptr, "match") && j >= 2) {
            pat = c->argv[i+1]->ptr;//pattern字符串
            patlen = sdslen(pat); //pattern字符串长度

            /* The pattern always matches if it is exactly "*", so it is
             * equivalent to disabling it. */
             // 如果pattern是"*",就不用匹配,全部返回,设置为0
            use_pattern = !(pat[0] == '*' && patlen == 1);

            i += 2;
        } else {
            addReply(c,shared.syntaxerr);
            goto cleanup;
        }
    }

    /* Step 2: Iterate the collection.
     *
     * Note that if the object is encoded with a ziplist, intset, or any other
     * representation that is not a hash table, we are sure that it is also
     * composed of a small number of elements. So to avoid taking state we
     * just return everything inside the object in a single call, setting the
     * cursor to zero to signal the end of the iteration. */
    // 如果对象的底层实现为 ziplist 、intset 而不是哈希表,
     // 那么这些对象应该只包含了少量元素,
     // 为了保持不让服务器记录迭代状态的设计
     // 我们将 ziplist 或者 intset 里面的所有元素都一次返回给调用者
     // 并向调用者返回游标(cursor) 0
    /* Handle the case of a hash table. */
    ht = NULL;
    if (o == NULL) {      // 迭代目标为数据库
        ht = c->db->dict;
    } else if (o->type == OBJ_SET && o->encoding == OBJ_ENCODING_HT) {
    	   //  迭代目标为 HT 编码的集合
        ht = o->ptr;
    } else if (o->type == OBJ_HASH && o->encoding == OBJ_ENCODING_HT) {
    	 // 迭代目标是HT编码的哈希对象
        ht = o->ptr;
        count *= 2; /* We return key / value for this type. */
    } else if (o->type == OBJ_ZSET && o->encoding == OBJ_ENCODING_SKIPLIST) {
    	  // 迭代目标是skiplist编码的有序集合对象
        zset *zs = o->ptr;
        ht = zs->dict;
        count *= 2; /* We return key / value for this type. */
    }

    if (ht) {
        void *privdata[2];
        /* We set the max number of iterations to ten times the specified
         * COUNT, so if the hash table is in a pathological state (very
         * sparsely populated) we avoid to block too much time at the cost
         * of returning no or very few elements. */
         // 设置最大的迭代长度为10*count次
        long maxiterations = count*10;

        /* We pass two pointers to the callback: the list to which it will
         * add new elements, and the object containing the dictionary so that
         * it is possible to fetch more data in a type-dependent way. */
         // 我们向回调函数传入两个指针:
        // 一个是用于记录被迭代元素的列表
        // 另一个是字典对象
        // 从而实现类型无关的数据提取操作
        privdata[0] = keys;
        privdata[1] = o;
        do {
        	 // 循环扫描ht,从游标cursor开始,调用指定的scanCallback函数,提出ht中的数据到刚开始创建的列表keys中
            cursor = dictScan(ht, cursor, scanCallback, privdata);
        } while (cursor &&
              maxiterations-- &&
              listLength(keys) < (unsigned long)count);
              //没迭代完,或没迭代够count,就继续循环
    } else if (o->type == OBJ_SET) {
    	// 如果是集合对象但编码不是HT是整数集合
        int pos = 0;
        int64_t ll;
        // 将整数值取出来,构建成字符串对象加入到keys列表中,游标设置为0,表示迭代完成
        while(intsetGet(o->ptr,pos++,&ll))
            listAddNodeTail(keys,createStringObjectFromLongLong(ll));
        cursor = 0;
    } else if (o->type == OBJ_HASH || o->type == OBJ_ZSET) {
    	// 如果是哈希对象,或有序集合对象,但是编码都不是HT,是ziplist
        unsigned char *p = ziplistIndex(o->ptr,0);
        unsigned char *vstr;
        unsigned int vlen;
        long long vll;

        while(p) {
        	   // 将值取出来,根据不同类型的值,构建成相同的字符串对象,加入到keys列表中
            ziplistGet(p,&vstr,&vlen,&vll);
            listAddNodeTail(keys,
                (vstr != NULL) ? createStringObject((char*)vstr,vlen) :
                                 createStringObjectFromLongLong(vll));
            p = ziplistNext(o->ptr,p);
        }
        cursor = 0;
    } else {
        serverPanic("Not handled encoding in SCAN.");
    }

    /* Step 3: Filter elements. */
     // 3. 如果设置MATCH参数,要进行过滤
    node = listFirst(keys); //链表首节点地址
    while (node) {
        robj *kobj = listNodeValue(node);   //key对象
        nextnode = listNextNode(node);    //下一个节点地址
        int filter = 0;   //默认为不过滤

        /* Filter element if it does not match the pattern. */
              //pattern不是"*"因此要过滤
        if (!filter && use_pattern) {
        	    // 如果kobj是字符串对象
            if (sdsEncodedObject(kobj)) {
            	  // kobj的值不匹配pattern,设置过滤标志
                if (!stringmatchlen(pat, patlen, kobj->ptr, sdslen(kobj->ptr), 0))
                    filter = 1;
            } else {
            	   // 如果kobj是整数对象
                char buf[LONG_STR_SIZE];
                int len;

                serverAssert(kobj->encoding == OBJ_ENCODING_INT);
                // 将整数转换为字符串类型,保存到buf中
                len = ll2string(buf,sizeof(buf),(long)kobj->ptr);
                  //buf的值不匹配pattern,设置过滤标志
                if (!stringmatchlen(pat, patlen, buf, len, 0)) filter = 1;
            }
        }

        /* Filter element if it is an expired key. */
         // 迭代目标是数据库,如果kobj是过期键,则过滤
        if (!filter && o == NULL && expireIfNeeded(c->db, kobj)) filter = 1;

        /* Remove the element and its associted value if needed. */
         // 如果该键满足了上述的过滤条件,那么将其从keys列表删除并释放
        if (filter) {
            decrRefCount(kobj);
            listDelNode(keys, node);
        }

        /* If this is a hash or a sorted set, we have a flat list of
         * key-value elements, so if this element was filtered, remove the
         * value, or skip it if it was not filtered: we only match keys. */
         //如果是hash 或者 zset 有序集合,因此keys列表中保存的是键值对,如果key键对象被过滤,值对象也应当被过滤
        if (o && (o->type == OBJ_ZSET || o->type == OBJ_HASH)) {
            node = nextnode;
            nextnode = listNextNode(node);//值对象的节点地址
            if (filter) {    // 如果该键满足了上述的过滤条件,那么将其从keys列表删除并释放
                kobj = listNodeValue(node); //取出值对象
                decrRefCount(kobj);
                listDelNode(keys, node); //删除
            }
        }
        node = nextnode;
    }

    /* Step 4: Reply to the client. */
      // 4. 回复信息给client
    addReplyMultiBulkLen(c, 2);   //2部分,一个是游标,一个是列表
    addReplyBulkLongLong(c,cursor); //回复游标

    addReplyMultiBulkLen(c, listLength(keys)); //回复列表长度
       //循环回复列表中的元素,并释放
    while ((node = listFirst(keys)) != NULL) {
        robj *kobj = listNodeValue(node);
        addReplyBulk(c, kobj);
        decrRefCount(kobj);
        listDelNode(keys, node);
    }

cleanup:// 清理代码
    listSetFreeMethod(keys,decrRefCountVoid);//设置特定的释放列表的方式decrRefCountVoid
    listRelease(keys);//释放
}

主要步骤如下:

  • 解析count和match参数.如果没有指定count,默认返回10条数据
  • 开始迭代集合,如果是key保存为ziplist或者intset,则一次性返回所有数据,没有游标(游标值直接返回0).由于redis设计只有数据量比较小的时候才会保存为ziplist或者intset,所以此处不会影响性能.
    游标在保存为hash的时候发挥作用,具体入口函数为dictScan,下文详细描述。
  • 根据match参数过滤返回值,并且如果这个键已经过期也会直接过滤掉(redis中键过期之后并不会立即删除)
  • 返回结果到客户端,是一个数组,第一个值是游标,第二个值是具体的键值对

有的细节我还没太看明白。

参考:

https://blog.csdn.net/men_wen/article/details/71088263

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值