redis学习笔记(13)---列表命令及实现

列表命令

Redis中的List对象的类型为REDIS_LIST,是一种双向链表结构,主要支持以下几种命令:

  1. LPUSH 向列表左端添加元素,用法:LPUSH key value
  2. RPUSH 向列表右端添加元素,用法:RPUSH key value
  3. LPOP 从列表左端弹出元素,用法:LPOP key
  4. RPOP 从列表右端弹出元素,用法:RPOP key
  5. LLEN 获取列表中元素个数,用法:LLEN key
  6. LRANGE 获取列表中某一片段的元素,用法:LRANGE key start stop,index从0开始,-1表示最后一个元素
  7. LREM 删除列表中指定的值,用法:LREM key count value,删除列表中前count个值为value的元素,当count>0时从左边开始数,count<0时从右边开始数,count=0时会删除所有值为value的元素
  8. LINDEX 获取指定索引的元素值,用法:LINDEX key index
  9. LSET 设置指定索引的元素值,用法:LSET key index value
  10. LTRIM 只保留列表指定片段,用法:LTRIM key start stop,包含start和stop
  11. LINSERT 像列表中插入元素,用法:LINSERT key BEFORE|AFTER privot value,从左边开始寻找值为privot的第一个元素,然后根据第二个参数是BEFORE还是AFTER决定在该元素的前面还是后面插入value
  12. RPOPLPUSH 将元素从一个列表转义到另一个列表,用法:RPOPLPUSH source destination

   除此之外,还有一些阻塞式命令:
  
   BLPOP
   BRPOP
   BRPOPLPUSH
 

编码方式

List的相关操作主要定义在t_list.c中。

列表对象支持两种编码方式:

#define REDIS_ENCODING_LINKEDLIST 4 
#define REDIS_ENCODING_ZIPLIST 5 
即底层列表可以通过ziplist或LinkedList来实现,两种链表的实现分别在ziplist.c和adlist.c中

命令的具体实现

在上一章中,我们已经知道当有一个客户端请求到来时

  1. server端会调用readQueryFromClient()来进行处理
  2. 其中首先会调用processInputBuffer()来解析输入请求,并为每一个参数创建一个字符串对象
  3. 调用processCommand并最终调用call来执行命令
由于在call命令之前的处理与字符串命令基本相同,因此本文只介绍调用call命令之后的处理。

1、push命令 (lpush、rpush、lpushx、rpushx)

1.1、lpush & rpush

eg:LPUSH key value [value …]
lpush将一个或多个值 value 插入到列表 key 的表头,rpush则是插入到表的尾部
当 key 存在但不是列表类型时,返回一个错误
如果 key 不存在,一个空列表会被创建并执行 LPUSH 操作

   在调用 lpush key val1 val2 val3时, redis是从前到后依次将每一个value插入到list的头部的。
   即先将val1插入到头部,再将val2插入到头部,最后将val3插入到头部,因此最后val3会在list的头部。
   即最终在list中的先后顺序是:
  表头—>val3—>val2—>val1—>表尾

1.2、lpushx & rpushx

eg:LPUSHX key value
lpushx将value 插入到列表 key 的表头, rpushx则插入到表的尾部
当且仅当 key 存在并且是一个列表
和 LPUSH 命令相反,当 key 不存在时, LPUSHX 命令什么也不做

  以lpush为例对整个流程进行简单分析  
  以 lpush zoo tiger为例,在调用call之前,redis已经为这3个参数分别创建了一个字符串对象,示意图如下:
  redis学习笔记(13)---列表命令及实现_第1张图片
  然后在call中会调用lpushCommand()进行处理
  1)当key(zoo)在数据库中存在,且不为REDIS_LIST类型时,错误,不能执行
  2)从argv[2]开始为每个对象调用tryObjectEncoding
  3)当key不存在时,创建一个类型为REDIS_LIST、编码方式为ziplist的对象lobj,并将key-lobj对插入到数据库中。即key仍为字符串类型,但是对应的value为list类型
  4)调用listTypePush 依次将每个value插入到链表lobj中
  5)调用addReplyLongLong 将操作的结果返回给client

void lpushCommand(redisClient *c) {
    pushGenericCommand(c,REDIS_HEAD);
}
void pushGenericCommand(redisClient *c, int where) {
    robj *lobj = lookupKeyWrite(c->db,c->argv[1]); //argv[1]为zoo

    if (lobj && lobj->type != REDIS_LIST) {  //key存在,但不为list类型,错误
        addReply(c,shared.wrongtypeerr);
        return;
    }
    for (j = 2; j < c->argc; j++) {
        c->argv[j] = tryObjectEncoding(c->argv[j]); 
        if (!lobj) {  //key不存在,创建一个list对象lobj
            lobj = createZiplistObject();
            dbAdd(c->db,c->argv[1],lobj);  //将key-lobj对插入到数据库中
        }
        listTypePush(lobj,c->argv[j],where); //依次将每个value插入到链表lobj
        pushed++;
    }
    addReplyLongLong(c, waiting + (lobj ? listTypeLength(lobj) : 0)); //返回结果给client
}

  所有的key-value对最终都是插入到c->db中的,因此每个db中的key必须都是唯一的,不管数据的底层实现是哪种类型。
  当lobj==NULL时,表明当前数据库中还没有zoo这个key,此时就需要创建一个list类型的对象lobj,并将key-lobj插入到数据库中。
  
  
  redis学习笔记(13)---列表命令及实现_第2张图片
  然后在listTypePush()中依次向list中插入每一个value  

void listTypePush(robj *subject, robj *value, int where) {
    /* 判断是否需要将ziplist转换为LinkedList */
    listTypeTryConversion(subject,value);
    if (subject->encoding == REDIS_ENCODING_ZIPLIST &&
        ziplistLen(subject->ptr) >= server.list_max_ziplist_entries)
            listTypeConvert(subject,REDIS_ENCODING_LINKEDLIST);

     // 将value加入到subject中
    if (subject->encoding == REDIS_ENCODING_ZIPLIST) {
        int pos = (where == REDIS_HEAD) ? ZIPLIST_HEAD : ZIPLIST_TAIL;
        value = getDecodedObject(value);
        subject->ptr = ziplistPush(subject->ptr,value->ptr,sdslen(value->ptr),pos); 
    } else if (subject->encoding == REDIS_ENCODING_LINKEDLIST) {
        if (where == REDIS_HEAD) {
            listAddNodeHead(subject->ptr,value);
        } else {
            listAddNodeTail(subject->ptr,value);
        }
    }
}

默认情况下List使用ziplist编码 ,当满足下面两个条件之一时会转变为LinkedList编码:

  1. 当待添加的新字符串长度 超过server.list_max_ziplist_value (默认为64)时
  2. ziplist中的节点数量 超过server.list_max_ziplist_entries(默认为512)时

  可以发现:
  1)当lobj为ziplist时,会调用ziplistPush()
  2)当lobj为LinkedList时,会调用listAddNodeHead()或listAddNodeTail()
  这样就将每一个value插入到list中了。
  

2、pop命令(lpop、rpop)

  LPOP key   移除并返回列表 key 的头元素,当 key 不存在时,返回 nil   RPOP则相反,返回的是列表的尾部元素
     以lpop为例进行分析    首先在数据库中查找key对应的列表,当列表不存在或key对应的value不为list类型时,直接返回   否则调用listTypePop从key对应的链表中弹出元素value   将操作结果返回给client
void lpopCommand(redisClient *c) {
    popGenericCommand(c,REDIS_HEAD); //对头部元素进行处理
} 

void popGenericCommand(redisClient *c, int where) {
    robj *o = lookupKeyWriteOrReply(c,c->argv[1],shared.nullbulk);
    if (o == NULL || checkType(c,o,REDIS_LIST)) return;

    robj *value = listTypePop(o,where);
    if (value == NULL) {
        addReply(c,shared.nullbulk);
    } else {
        char *event = (where == REDIS_HEAD) ? "lpop" : "rpop";
        addReplyBulk(c,value);
    }
}

3、linsert

LINSERT key BEFORE|AFTER pivot value
将value插入到key列表中元素pivot的before或after

  首先根据before、after来判断是插入到头部还是尾部
  然后调用pushxGenericCommand完成插入操作

void linsertCommand(redisClient *c) {
    c->argv[4] = tryObjectEncoding(c->argv[4]);
    if (strcasecmp(c->argv[2]->ptr,"after") == 0) {
        pushxGenericCommand(c,c->argv[3],c->argv[4],REDIS_TAIL);
    } else if (strcasecmp(c->argv[2]->ptr,"before") == 0) {
        pushxGenericCommand(c,c->argv[3],c->argv[4],REDIS_HEAD);
    } else {
        addReply(c,shared.syntaxerr);
    }
} 

  在pushxGenericCommand中
  1)首先在db中查找key对应的列表,当列表不存在或元素类型不为list时,直接返回
  2)当相对元素pivot不为空时
    a)首先找到元素pivot对应的位置
    b)若能找到,则调用listTypeInsert插入元素
    c)返回操作结果
  3)若pivot为NULL时,直接调用listTypePush完成插入操作,并返回结果      

void pushxGenericCommand(redisClient *c, robj *refval, robj *val, int where) {

    if ((subject = lookupKeyReadOrReply(c,c->argv[1],shared.czero)) == NULL ||
        checkType(c,subject,REDIS_LIST)) return;   //1)查找key

    if (refval != NULL) { //2)pivot不为空时
        listTypeTryConversion(subject,val);

        iter = listTypeInitIterator(subject,0,REDIS_TAIL);
        while (listTypeNext(iter,&entry)) {  
            if (listTypeEqual(&entry,refval)) {//2.a)找到pivot元素的位置
                listTypeInsert(&entry,val,where); //2.b)插入value
                inserted = 1;  //插入完成
                break;
            }
        }
        if (inserted) {  //2.c)返回操作成功的结果
            if (subject->encoding == REDIS_ENCODING_ZIPLIST &&
                ziplistLen(subject->ptr) > server.list_max_ziplist_entries)
                    listTypeConvert(subject,REDIS_ENCODING_LINKEDLIST);
        } else {//2.c)返回操作失败的结果
            addReply(c,shared.cnegone);
            return;
        }
    } else {  //3)插入元素
        listTypePush(subject,val,where);
    }
    addReplyLongLong(c,listTypeLength(subject)); //返回操作结果
}

4、llen

LLEN key
返回列表key的长度

  首先在数据库中查找key对应的列表,当列表不存在或key对应的value不为list类型时,直接返回
  否则调用listTypeLength()获取列表长度
  并将结果返回给client

void llenCommand(redisClient *c) {
    robj *o = lookupKeyReadOrReply(c,c->argv[1],shared.czero);
    if (o == NULL || checkType(c,o,REDIS_LIST)) return;
    addReplyLongLong(c,listTypeLength(o));
}

5、lindex

LINDEX key index
返回列表key中第index个元素,从0开始计数
index可以为负值
void lindexCommand(redisClient *c) {
    robj *o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk);
    if (o == NULL || checkType(c,o,REDIS_LIST)) return;
    if ((getLongFromObjectOrReply(c, c->argv[2], &index, NULL) != REDIS_OK))
        return;

    if (o->encoding == REDIS_ENCODING_ZIPLIST) { //ziplist
        p = ziplistIndex(o->ptr,index);
        if (ziplistGet(p,&vstr,&vlen,&vlong)) {
            if (vstr) {
                value = createStringObject((char*)vstr,vlen);
            } else {
                value = createStringObjectFromLongLong(vlong);
            }
            addReplyBulk(c,value);
        } else {
            addReply(c,shared.nullbulk);
        }
    } else if (o->encoding == REDIS_ENCODING_LINKEDLIST) { //LinkedList
        listNode *ln = listIndex(o->ptr,index);
        if (ln != NULL) {
            value = listNodeValue(ln);
            addReplyBulk(c,value);
        } else {
            addReply(c,shared.nullbulk);
        }
    } 
}

总结

  对于列表类型的插入、弹出及其他等命令,一般的执行过程都是:
  1)在db中查找key对应的列表,可以分为列表不存在,元素存在但不为list、列表存在这三种情况
  2)然后根据三种情况,对命令继续处理,包括直接返回或创建一个新链表等
  3)在列表存在时,对list执行push、pop、insert、length等操作
  4)将操作的结果返回给client

  之前已经对字符串命令的实现进行了分析,除了字符串、列表之外,还有哈希表、集合、有序集合这几种底层实现。
  这几种类型对应的命令实现方式与字符串、列表基本类似,其实现分别在t_hash.c 、t_set.c 、t_zset.c这几个文件中。
  server收到客户端的请求后,都会在call函数中执行这些命令。



本文所引用的源码全部来自Redis3.0.7版本

你可能感兴趣的:(学习笔记整理,redis)