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

来源:互联网 发布:google chrome mac下载 编辑:程序博客网 时间:2024/05/21 11:04

列表命令

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个参数分别创建了一个字符串对象,示意图如下:
  这里写图片描述
  然后在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插入到数据库中。
  
  
  这里写图片描述
  然后在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版本

0 0
原创粉丝点击