Redis使用及源码剖析-9.Redis键操作函数-2021-1-26

2022-02-22 13:40:17 浏览数 (1)

文章目录

  • 前言
  • 一、Redis客户端结构体简介
  • 二、字符串键函数
    • 1.set系列函数
    • 2.incr decr函数
  • 三、列表键函数
    • 1.添加元素函数
    • 2.设置指定位置索引函数
    • 3.获取列表范围元素的函数
  • 四、哈希键函数
    • 1.获取指定字段的值
    • 2.获取哈希表容量
  • 五、集合键函数
    • 1.向集合添加元素
    • 2.判断元素是否在集合内部
  • 六、有序集合键函数
    • 1.从有序集合删除元素
    • 2.获取指定元素分值
  • 总结

前言

前面已经了解到Redis包括五种类型的对象:字符串对象、列表对象、哈希对象、集合对象和有序集合对象。对象的底层由SDS、双向链表、压缩列表、跳表、词典和整数集合等数据结构实现。Redis是一个key-value类型的数据库。其中的key均为字符串对象,value可以是五种对象的任意一种,根据value类型不同将键值对称为某某键,如value是字符串对象时为字符串键。本节就简单介绍一下五种Redis键对应的一些操作API。

一、Redis客户端结构体简介

Redis为每一个客户端定义了redisClient 对象,包括客户端套接字,客户端输入的命令参数数目,和参数数组等。这样服务端就可以根据具体参数执行对应操作,并且将结果通过套接字返回给客户端。

代码语言:javascript复制
typedef struct redisClient {
    // 套接字描述符
    int fd;
    // 参数数量
    int argc;

    // 参数对象数组
    robj **argv;
    .....
} redisClient;

二、字符串键函数

字符串键是Redis中最常见的一些键,字符串键常见的命令有set get incr decr等,具体可见第一篇文章: https://blog.csdn.net/qq_37292982/article/details/112704295. 字符串键的相关代码在t_string.c中,现在选取部分API进行介绍。

1.set系列函数

set系列函数如下所示:

代码语言:javascript复制
SET key value //设置key value,已经存在时覆盖
SETEX key seconds value //设置key value,已经存在时覆盖,
//并且设置过期时间
SETNX key value //只在key不存在时设置value
MSET key1 value1 key2 value2 //批量设置

Redis为所有set函数定义了统一入口setCommand,在这个函数内部分析函数参数,最后调用具体的set函数setGenericCommand。

代码语言:javascript复制
/* SET key value [NX] [XX] [EX <seconds>] [PX <milliseconds>] */
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] == '') {
            flags |= REDIS_SET_NX;
        } else if ((a[0] == 'x' || a[0] == 'X') &&
                   (a[1] == 'x' || a[1] == 'X') && a[2] == '') {
            flags |= REDIS_SET_XX;
        } else if ((a[0] == 'e' || a[0] == 'E') &&
                   (a[1] == 'x' || a[1] == 'X') && a[2] == '' && next) {
            unit = UNIT_SECONDS;
            expire = next;
            j  ;
        } else if ((a[0] == 'p' || a[0] == 'P') &&
                   (a[1] == 'x' || a[1] == 'X') && a[2] == '' && next) {
            unit = UNIT_MILLISECONDS;
            expire = next;
            j  ;
        } else {
            addReply(c,shared.syntaxerr);
            return;
        }
    }

    // 尝试对值对象进行编码
    c->argv[2] = tryObjectEncoding(c->argv[2]);

    setGenericCommand(c,flags,c->argv[1],
    c->argv[2],expire,unit,NULL,NULL);
}

setGenericCommand根据传入参数执行对应操作:

代码语言:javascript复制
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 参数的值
        // T = O(N)
        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);
}

mset命令实现函数为msetGenericCommand,如下所示:

代码语言:javascript复制
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);
}

2.incr decr函数

incr decr命令实现的函数如下所示:

代码语言:javascript复制
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);
}

三、列表键函数

列表键的底层实现为双向列表或者压缩列表,常见命令可见第一篇文章: https://blog.csdn.net/qq_37292982/article/details/112704295. 列表键的相关代码在t_list.c中,现在选取部分API进行介绍。

1.添加元素函数

lpush和rpush命令可以在一个列表的左端或者右端添加元素,其实现如下:先根据要添加对象的长度以及列表元素数目判断一下是否需要将压缩列表转为双端链表,然后根据不同的底层实现调用压缩列表和双向链表的api进行元素添加即可。

代码语言:javascript复制
/* The function pushes an element to the specified list object 'subject',
 * at head or tail position as specified by 'where'.
 *
 * 将给定元素添加到列表的表头或表尾。
 *
 * 参数 where 决定了新元素添加的位置:
 *
 *  - REDIS_HEAD 将新元素添加到表头
 *
 *  - REDIS_TAIL 将新元素添加到表尾
 *
 * There is no need for the caller to increment the refcount of 'value' as
 * the function takes care of it if needed. 
 *
 * 调用者无须担心 value 的引用计数,因为这个函数会负责这方面的工作。
 */
void listTypePush(robj *subject, robj *value, int where) {

    /* Check if we need to convert the ziplist */
    // 是否需要转换编码?
    listTypeTryConversion(subject,value);

    if (subject->encoding == REDIS_ENCODING_ZIPLIST &&
        ziplistLen(subject->ptr) >= server.list_max_ziplist_entries)
            listTypeConvert(subject,REDIS_ENCODING_LINKEDLIST);

    // ZIPLIST
    if (subject->encoding == REDIS_ENCODING_ZIPLIST) {
        int pos = (where == REDIS_HEAD) ? ZIPLIST_HEAD : ZIPLIST_TAIL;
        // 取出对象的值,因为 ZIPLIST 只能保存字符串或整数
        value = getDecodedObject(value);
        subject->ptr = ziplistPush(subject->ptr,value->ptr,sdslen(value->ptr),pos);
        decrRefCount(value);

    // 双端链表
    } else if (subject->encoding == REDIS_ENCODING_LINKEDLIST) {
        if (where == REDIS_HEAD) {
            listAddNodeHead(subject->ptr,value);
        } else {
            listAddNodeTail(subject->ptr,value);
        }
        incrRefCount(value);

    // 未知编码
    } else {
        redisPanic("Unknown list encoding");
    }
}

具体调用过程如下,先通过lpushCommand和rpushCommand调用pushGenericCommand函数,在pushGenericCommand内部遍历所有参数,调用listTypePush添加到列表中。

代码语言:javascript复制
void pushGenericCommand(redisClient *c, int where) {

    int j, waiting = 0, pushed = 0;

    // 取出列表对象
    robj *lobj = lookupKeyWrite(c->db,c->argv[1]);

    // 如果列表对象不存在,那么可能有客户端在等待这个键的出现
    int may_have_waiting_clients = (lobj == NULL);

    if (lobj && lobj->type != REDIS_LIST) {
        addReply(c,shared.wrongtypeerr);
        return;
    }

    // 将列表状态设置为就绪
    if (may_have_waiting_clients) signalListAsReady(c,c->argv[1]);

    // 遍历所有输入值,并将它们添加到列表中
    for (j = 2; j < c->argc; j  ) {

        // 编码值
        c->argv[j] = tryObjectEncoding(c->argv[j]);

        // 如果列表对象不存在,那么创建一个,并关联到数据库
        if (!lobj) {
            lobj = createZiplistObject();
            dbAdd(c->db,c->argv[1],lobj);
        }

        // 将值推入到列表
        listTypePush(lobj,c->argv[j],where);

        pushed  ;
    }

    // 返回添加的节点数量
    addReplyLongLong(c, waiting   (lobj ? listTypeLength(lobj) : 0));

    // 如果至少有一个元素被成功推入,那么执行以下代码
    if (pushed) {
        char *event = (where == REDIS_HEAD) ? "lpush" : "rpush";

        // 发送键修改信号
        signalModifiedKey(c->db,c->argv[1]);

        // 发送事件通知
        notifyKeyspaceEvent(REDIS_NOTIFY_LIST,event,c->argv[1],c->db->id);
    }

    server.dirty  = pushed;
}

void lpushCommand(redisClient *c) {
    pushGenericCommand(c,REDIS_HEAD);
}

void rpushCommand(redisClient *c) {
    pushGenericCommand(c,REDIS_TAIL);
}

2.设置指定位置索引函数

lset key index value函数可以设置列表指定索引的值,具体实现函数如下:

代码语言:javascript复制
void lsetCommand(redisClient *c) {

    // 取出列表对象
    robj *o = lookupKeyWriteOrReply(c,c->argv[1],shared.nokeyerr);

    if (o == NULL || checkType(c,o,REDIS_LIST)) return;
    long index;

    // 取出值对象 value
    robj *value = (c->argv[3] = tryObjectEncoding(c->argv[3]));

    // 取出整数值对象 index
    if ((getLongFromObjectOrReply(c, c->argv[2], &index, NULL) != REDIS_OK))
        return;

    // 查看保存 value 值是否需要转换列表的底层编码
    listTypeTryConversion(o,value);

    // 设置到 ziplist
    if (o->encoding == REDIS_ENCODING_ZIPLIST) {
        unsigned char *p, *zl = o->ptr;
        // 查找索引
        p = ziplistIndex(zl,index);
        if (p == NULL) {
            addReply(c,shared.outofrangeerr);
        } else {
            // 删除现有的值
            o->ptr = ziplistDelete(o->ptr,&p);
            // 插入新值到指定索引
            value = getDecodedObject(value);
            o->ptr = ziplistInsert(o->ptr,p,value->ptr,sdslen(value->ptr));
            decrRefCount(value);

            addReply(c,shared.ok);
            signalModifiedKey(c->db,c->argv[1]);
            notifyKeyspaceEvent(REDIS_NOTIFY_LIST,"lset",c->argv[1],c->db->id);
            server.dirty  ;
        }

    // 设置到双端链表
    } else if (o->encoding == REDIS_ENCODING_LINKEDLIST) {

        listNode *ln = listIndex(o->ptr,index);

        if (ln == NULL) {
            addReply(c,shared.outofrangeerr);
        } else {
            // 删除旧值对象
            decrRefCount((robj*)listNodeValue(ln));
            // 指向新对象
            listNodeValue(ln) = value;
            incrRefCount(value);

            addReply(c,shared.ok);
            signalModifiedKey(c->db,c->argv[1]);
            notifyKeyspaceEvent(REDIS_NOTIFY_LIST,"lset",c->argv[1],c->db->id);
            server.dirty  ;
        }
    } else {
        redisPanic("Unknown list encoding");
    }
}

3.获取列表范围元素的函数

lrange key start stop函数可以获取列表指定范围的元素,具体实现函数如下:

代码语言:javascript复制
void lrangeCommand(redisClient *c) {
    robj *o;
    long start, end, llen, rangelen;

    // 取出索引值 start 和 end
    if ((getLongFromObjectOrReply(c, c->argv[2], &start, NULL) != REDIS_OK) ||
        (getLongFromObjectOrReply(c, c->argv[3], &end, NULL) != REDIS_OK)) return;

    // 取出列表对象
    if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.emptymultibulk)) == NULL
         || checkType(c,o,REDIS_LIST)) return;

    // 取出列表长度
    llen = listTypeLength(o);

    /* convert negative indexes */
    // 将负数索引转换成正数索引
    if (start < 0) start = llen start;
    if (end < 0) end = llen end;
    if (start < 0) start = 0;

    /* Invariant: start >= 0, so this test will be true when end < 0.
     * The range is empty when start > end or start >= length. */
    if (start > end || start >= llen) {
        addReply(c,shared.emptymultibulk);
        return;
    }
    if (end >= llen) end = llen-1;
    rangelen = (end-start) 1;

    /* Return the result in form of a multi-bulk reply */
    addReplyMultiBulkLen(c,rangelen);

    if (o->encoding == REDIS_ENCODING_ZIPLIST) {
        unsigned char *p = ziplistIndex(o->ptr,start);
        unsigned char *vstr;
        unsigned int vlen;
        long long vlong;

        // 遍历 ziplist ,并将指定索引上的值添加到回复中
        while(rangelen--) {
            ziplistGet(p,&vstr,&vlen,&vlong);
            if (vstr) {
                addReplyBulkCBuffer(c,vstr,vlen);
            } else {
                addReplyBulkLongLong(c,vlong);
            }
            p = ziplistNext(o->ptr,p);
        }

    } else if (o->encoding == REDIS_ENCODING_LINKEDLIST) {
        listNode *ln;

        /* If we are nearest to the end of the list, reach the element
         * starting from tail and going backward, as it is faster. */
        if (start > llen/2) start -= llen;
        ln = listIndex(o->ptr,start);

        // 遍历双端链表,将指定索引上的值添加到回复
        while(rangelen--) {
            addReplyBulk(c,ln->value);
            ln = ln->next;
        }

    } else {
        redisPanic("List encoding is not LINKEDLIST nor ZIPLIST!");
    }
}

四、哈希键函数

哈希键的底层实现为哈希表或者压缩列表,常见命令可见第一篇文章: https://blog.csdn.net/qq_37292982/article/details/112704295. 哈希键的相关代码在t_hash.c中,现在选取部分API进行介绍。

1.获取指定字段的值

hget key field命令可可以获取指定字段的值,实现函数如下:其中hgetCommand函数调用addHashFieldToReply函数,addHashFieldToReply负责取出字段对应值并添加到回复,它键根据底层实现是压缩列表和哈希表的不同分别调用hashTypeGetFromZiplist和hashTypeGetFromHashTable获取值。

代码语言:javascript复制
/* Get the value from a ziplist encoded hash, identified by field.
 * Returns -1 when the field cannot be found. 
 *
 * 从 ziplist 编码的 hash 中取出和 field 相对应的值。
 *
 * 参数:
 *  field   域
 *  vstr    值是字符串时,将它保存到这个指针
 *  vlen    保存字符串的长度
 *  ll      值是整数时,将它保存到这个指针
 *
 * 查找失败时,函数返回 -1 。
 * 查找成功时,返回 0 。
 */
int hashTypeGetFromZiplist(robj *o, robj *field,
                           unsigned char **vstr,
                           unsigned int *vlen,
                           long long *vll)
{
    unsigned char *zl, *fptr = NULL, *vptr = NULL;
    int ret;

    // 确保编码正确
    redisAssert(o->encoding == REDIS_ENCODING_ZIPLIST);

    // 取出未编码的域
    field = getDecodedObject(field);

    // 遍历 ziplist ,查找域的位置
    zl = o->ptr;
    fptr = ziplistIndex(zl, ZIPLIST_HEAD);
    if (fptr != NULL) {
        // 定位包含域的节点
        fptr = ziplistFind(fptr, field->ptr, sdslen(field->ptr), 1);
        if (fptr != NULL) {
            /* Grab pointer to the value (fptr points to the field) */
            // 域已经找到,取出和它相对应的值的位置
            vptr = ziplistNext(zl, fptr);
            redisAssert(vptr != NULL);
        }
    }

    decrRefCount(field);

    // 从 ziplist 节点中取出值
    if (vptr != NULL) {
        ret = ziplistGet(vptr, vstr, vlen, vll);
        redisAssert(ret);
        return 0;
    }

    // 没找到
    return -1;
}
/* Get the value from a hash table encoded hash, identified by field.
 * Returns -1 when the field cannot be found. 
 *
 * 从 REDIS_ENCODING_HT 编码的 hash 中取出和 field 相对应的值。
 *
 * 成功找到值时返回 0 ,没找到返回 -1 。
 */
int hashTypeGetFromHashTable(robj *o, robj *field, robj **value) {
    dictEntry *de;

    // 确保编码正确
    redisAssert(o->encoding == REDIS_ENCODING_HT);

    // 在字典中查找域(键)
    de = dictFind(o->ptr, field);

    // 键不存在
    if (de == NULL) return -1;

    // 取出域(键)的值
    *value = dictGetVal(de);

    // 成功找到
    return 0;
}
/*
 * 辅助函数:将哈希中域 field 的值添加到回复中
 */
static void addHashFieldToReply(redisClient *c, robj *o, robj *field) {
    int ret;

    // 对象不存在
    if (o == NULL) {
        addReply(c, shared.nullbulk);
        return;
    }

    // ziplist 编码
    if (o->encoding == REDIS_ENCODING_ZIPLIST) {
        unsigned char *vstr = NULL;
        unsigned int vlen = UINT_MAX;
        long long vll = LLONG_MAX;

        // 取出值
        ret = hashTypeGetFromZiplist(o, field, &vstr, &vlen, &vll);
        if (ret < 0) {
            addReply(c, shared.nullbulk);
        } else {
            if (vstr) {
                addReplyBulkCBuffer(c, vstr, vlen);
            } else {
                addReplyBulkLongLong(c, vll);
            }
        }

    // 字典
    } else if (o->encoding == REDIS_ENCODING_HT) {
        robj *value;

        // 取出值
        ret = hashTypeGetFromHashTable(o, field, &value);
        if (ret < 0) {
            addReply(c, shared.nullbulk);
        } else {
            addReplyBulk(c, value);
        }

    } else {
        redisPanic("Unknown hash encoding");
    }
}

void hgetCommand(redisClient *c) {
    robj *o;

    if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk)) == NULL ||
        checkType(c,o,REDIS_HASH)) return;

    // 取出并返回域的值
    addHashFieldToReply(c, o, c->argv[2]);
}

2.获取哈希表容量

hlen key 获取哈希表字段对数目,其实现函数如下:hlenCommand调用addReplyLongLong函数,addReplyLongLong函数的第二个参数字段对数目通过hashTypeLength函数获取,hashTypeLength函数根据底层实现分别调用压缩列表和哈希表对应函数获取数目。

代码语言:javascript复制
/* Return the number of elements in a hash. 
 *
 * 返回哈希表的 field-value 对数量
 */
unsigned long hashTypeLength(robj *o) {
    unsigned long length = ULONG_MAX;

    if (o->encoding == REDIS_ENCODING_ZIPLIST) {
        // ziplist 中,每个 field-value 对都需要使用两个节点来保存
        length = ziplistLen(o->ptr) / 2;
    } else if (o->encoding == REDIS_ENCODING_HT) {
        length = dictSize((dict*)o->ptr);
    } else {
        redisPanic("Unknown hash encoding");
    }

    return length;
}
void hlenCommand(redisClient *c) {
    robj *o;

    // 取出哈希对象
    if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.czero)) == NULL ||
        checkType(c,o,REDIS_HASH)) return;

    // 回复
    addReplyLongLong(c,hashTypeLength(o));
}

五、集合键函数

集合键的底层实现为整数集合或者哈希表,常见命令可见第一篇文章: https://blog.csdn.net/qq_37292982/article/details/112704295. 集合键的相关代码在t_set.c中,现在选取部分API进行介绍。

1.向集合添加元素

sadd key member1 member2命令可以向集合中添加元素,其实现如下:saddCommand命令调用setTypeAdd函数,setTypeAdd根据底层实现分别调用整数集合或者哈希表的函数添加元素。

代码语言:javascript复制
/*
 * 多态 add 操作
 *
 * 添加成功返回 1 ,如果元素已经存在,返回 0 。
 */
int setTypeAdd(robj *subject, robj *value) {
    long long llval;

    // 字典
    if (subject->encoding == REDIS_ENCODING_HT) {
        // 将 value 作为键, NULL 作为值,将元素添加到字典中
        if (dictAdd(subject->ptr,value,NULL) == DICT_OK) {
            incrRefCount(value);
            return 1;
        }

    // intset
    } else if (subject->encoding == REDIS_ENCODING_INTSET) {
        
        // 如果对象的值可以编码为整数的话,那么将对象的值添加到 intset 中
        if (isObjectRepresentableAsLongLong(value,&llval) == REDIS_OK) {
            uint8_t success = 0;
            subject->ptr = intsetAdd(subject->ptr,llval,&success);
            if (success) {
                /* Convert to regular set when the intset contains
                 * too many entries. */
                // 添加成功
                // 检查集合在添加新元素之后是否需要转换为字典
                if (intsetLen(subject->ptr) > server.set_max_intset_entries)
                    setTypeConvert(subject,REDIS_ENCODING_HT);
                return 1;
            }

        // 如果对象的值不能编码为整数,那么将集合从 intset 编码转换为 HT 编码
        // 然后再执行添加操作
        } else {
            /* Failed to get integer from object, convert to regular set. */
            setTypeConvert(subject,REDIS_ENCODING_HT);

            /* The set *was* an intset and this value is not integer
             * encodable, so dictAdd should always work. */
            redisAssertWithInfo(NULL,value,dictAdd(subject->ptr,value,NULL) == DICT_OK);
            incrRefCount(value);
            return 1;
        }

    // 未知编码
    } else {
        redisPanic("Unknown set encoding");
    }

    // 添加失败,元素已经存在
    return 0;
}
void saddCommand(redisClient *c) {
    robj *set;
    int j, added = 0;

    // 取出集合对象
    set = lookupKeyWrite(c->db,c->argv[1]);

    // 对象不存在,创建一个新的,并将它关联到数据库
    if (set == NULL) {
        set = setTypeCreate(c->argv[2]);
        dbAdd(c->db,c->argv[1],set);

    // 对象存在,检查类型
    } else {
        if (set->type != REDIS_SET) {
            addReply(c,shared.wrongtypeerr);
            return;
        }
    }

    // 将所有输入元素添加到集合中
    for (j = 2; j < c->argc; j  ) {
        c->argv[j] = tryObjectEncoding(c->argv[j]);
        // 只有元素未存在于集合时,才算一次成功添加
        if (setTypeAdd(set,c->argv[j])) added  ;
    }

    // 如果有至少一个元素被成功添加,那么执行以下程序
    if (added) {
        // 发送键修改信号
        signalModifiedKey(c->db,c->argv[1]);
        // 发送事件通知
        notifyKeyspaceEvent(REDIS_NOTIFY_SET,"sadd",c->argv[1],c->db->id);
    }

    // 将数据库设为脏
    server.dirty  = added;

    // 返回添加元素的数量
    addReplyLongLong(c,added);
}

2.判断元素是否在集合内部

sismember key member命令可以判断指定元素是否在集合内,其实现函数如下:sismemberCommand函数调用setTypeIsMember函数,setTypeIsMember根据底层实现分别调用哈希表和整数集合API。

代码语言:javascript复制
/*
 * 多态 ismember 操作
 */
int setTypeIsMember(robj *subject, robj *value) {
    long long llval;

    // HT
    if (subject->encoding == REDIS_ENCODING_HT) {
        return dictFind((dict*)subject->ptr,value) != NULL;

    // INTSET
    } else if (subject->encoding == REDIS_ENCODING_INTSET) {
        if (isObjectRepresentableAsLongLong(value,&llval) == REDIS_OK) {
            return intsetFind((intset*)subject->ptr,llval);
        }

    // 未知编码
    } else {
        redisPanic("Unknown set encoding");
    }

    // 查找失败
    return 0;
}
void sismemberCommand(redisClient *c) {
    robj *set;

    // 取出集合对象
    if ((set = lookupKeyReadOrReply(c,c->argv[1],shared.czero)) == NULL ||
        checkType(c,set,REDIS_SET)) return;

    // 编码输入元素
    c->argv[2] = tryObjectEncoding(c->argv[2]);

    // 检查是否存在
    if (setTypeIsMember(set,c->argv[2]))
        addReply(c,shared.cone);
    else
        addReply(c,shared.czero);
}

六、有序集合键函数

有序集合键的底层实现为压缩列表或者ZSET(跳表 哈希表),常见命令可见第一篇文章: https://blog.csdn.net/qq_37292982/article/details/112704295. 有序集合键的相关代码在t_zset.c中,现在选取部分API进行介绍。

1.从有序集合删除元素

ZREM key member命令可以从有序集合中删除元素,其实现函数如下:zremCommand函数根据底层实现不同调用压缩列表或者哈希表和跳表的API删除元素。

代码语言:javascript复制
/* Delete (element,score) pair from ziplist. Use local copy of eptr because we
 * don't want to modify the one given as argument. 
 *
 * 从 ziplist 中删除 eptr 所指定的有序集合元素(包括成员和分值)
 */
unsigned char *zzlDelete(unsigned char *zl, unsigned char *eptr) {
    unsigned char *p = eptr;

    /* TODO: add function to ziplist API to delete N elements from offset. */
    zl = ziplistDelete(zl,&p);
    zl = ziplistDelete(zl,&p);
    return zl;
}
void zremCommand(redisClient *c) {
    robj *key = c->argv[1];
    robj *zobj;
    int deleted = 0, keyremoved = 0, j;

    // 取出有序集合对象
    if ((zobj = lookupKeyWriteOrReply(c,key,shared.czero)) == NULL ||
        checkType(c,zobj,REDIS_ZSET)) return;

    // 从 ziplist 中删除
    if (zobj->encoding == REDIS_ENCODING_ZIPLIST) {
        unsigned char *eptr;

        // 遍历所有输入元素
        for (j = 2; j < c->argc; j  ) {
            // 如果元素在 ziplist 中存在的话
            if ((eptr = zzlFind(zobj->ptr,c->argv[j],NULL)) != NULL) {
                // 元素存在时,删除计算器才增一
                deleted  ;
                // 那么删除它们
                zobj->ptr = zzlDelete(zobj->ptr,eptr);
                
                // ziplist 已清空,将有序集合从数据库中删除
                if (zzlLength(zobj->ptr) == 0) {
                    dbDelete(c->db,key);
                    break;
                }
            }
        }

    // 从跳跃表和字典中删除
    } else if (zobj->encoding == REDIS_ENCODING_SKIPLIST) {
        zset *zs = zobj->ptr;
        dictEntry *de;
        double score;

        // 遍历所有输入元素
        for (j = 2; j < c->argc; j  ) {

            // 查找元素
            de = dictFind(zs->dict,c->argv[j]);

            if (de != NULL) {
                // 元素存在时,删除计算器才增一
                deleted  ;

                /* Delete from the skiplist */
                // 将元素从跳跃表中删除
                score = *(double*)dictGetVal(de);
                redisAssertWithInfo(c,c->argv[j],zslDelete(zs->zsl,score,c->argv[j]));

                /* Delete from the hash table */
                // 将元素从字典中删除
                dictDelete(zs->dict,c->argv[j]);

                // 检查是否需要缩小字典
                if (htNeedsResize(zs->dict)) dictResize(zs->dict);

                // 字典已被清空,有序集合已经被清空,将它从数据库中删除
                if (dictSize(zs->dict) == 0) {
                    dbDelete(c->db,key);
                    break;
                }
            }
        }
    } else {
        redisPanic("Unknown sorted set encoding");
    }

    // 如果有至少一个元素被删除的话,那么执行以下代码
    if (deleted) {

        notifyKeyspaceEvent(REDIS_NOTIFY_ZSET,"zrem",key,c->db->id);

        if (keyremoved)
            notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"del",key,c->db->id);

        signalModifiedKey(c->db,key);

        server.dirty  = deleted;
    }

    // 回复被删除元素的数量
    addReplyLongLong(c,deleted);
}

2.获取指定元素分值

zscore key member命令可以获取指定元素分值,其实现如下:zscoreCommand调用压缩列表或者跳表的API获取指定元素的分值并回复。

代码语言:javascript复制
void zscoreCommand(redisClient *c) {
    robj *key = c->argv[1];
    robj *zobj;
    double score;

    if ((zobj = lookupKeyReadOrReply(c,key,shared.nullbulk)) == NULL ||
        checkType(c,zobj,REDIS_ZSET)) return;

    // ziplist
    if (zobj->encoding == REDIS_ENCODING_ZIPLIST) {
        // 取出元素
        if (zzlFind(zobj->ptr,c->argv[2],&score) != NULL)
            // 回复分值
            addReplyDouble(c,score);
        else
            addReply(c,shared.nullbulk);

    // SKIPLIST
    } else if (zobj->encoding == REDIS_ENCODING_SKIPLIST) {
        zset *zs = zobj->ptr;
        dictEntry *de;

        c->argv[2] = tryObjectEncoding(c->argv[2]);
        // 直接从字典中取出并返回分值
        de = dictFind(zs->dict,c->argv[2]);
        if (de != NULL) {
            score = *(double*)dictGetVal(de);
            addReplyDouble(c,score);
        } else {
            addReply(c,shared.nullbulk);
        }

    } else {
        redisPanic("Unknown sorted set encoding");
    }
}

总结

本文对五种Redis键的常见操作函数进行了简单介绍,涉及的API较多,需要细细阅读。

0 人点赞