Redis中的List对象的类型为REDIS_LIST,是一种双向链表结构,主要支持以下几种命令:
LPUSH key value
RPUSH key value
LPOP key
RPOP key
LLEN key
LRANGE key start stop
,index从0开始,-1表示最后一个元素LREM key count value
,删除列表中前count个值为value的元素,当count>0时从左边开始数,count<0时从右边开始数,count=0时会删除所有值为value的元素LINDEX key index
LSET key index value
LTRIM key start stop
,包含start和stopLINSERT key BEFORE|AFTER privot value
,从左边开始寻找值为privot的第一个元素,然后根据第二个参数是BEFORE还是AFTER决定在该元素的前面还是后面插入valueRPOPLPUSH 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中
在上一章中,我们已经知道当有一个客户端请求到来时
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—>表尾
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)当lobj为ziplist时,会调用ziplistPush()
2)当lobj为LinkedList时,会调用listAddNodeHead()或listAddNodeTail()
这样就将每一个value插入到list中了。
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);
}
}
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)); //返回操作结果
}
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));
}
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版本