Redis学习笔记(4):list类型API大全
1. list类型简介
列表(list)类型是用来存储多个有序的字符串,列表中的每个字符串称为元素(element),一个列表最多可以存储2^32-1个元素。在Redis中,可以对列表两端插入(push)和弹出(pop),还可以获取指定范围的元素列表、获取指定索引下标的元素等。列表是一种比较灵活的数据结构,它可以充当栈和队列的角色,在实际开发上有很多应用场景。
列表类型有两个特点:第一、列表中的元素是有序的(跟插入顺序有关),这就意味着可以通过索引下标获取某个元素或者某个范围内的元素列,第二、列表中的元素可以是重复的。
注意,本篇的标题是list类型API大全,意思是Redis7.0 RC2版本中所有的list命令都会介绍到!
2. list所有命令用法与特性介绍
lpush/rpush命令:
lpush/rpush {key} {value1} {value2}… ,lpush为从列表左端加入元素,rpush为从列表右端加入元素,可以一次性输入多个value。返回值为列表内的元素个数。
# 示例
127.0.0.1:6379> lpush mylist a b
(integer) 2
127.0.0.1:6379> rpush mylist c
(integer) 3
lpushx/rpushx {key} {value1} {value2}… ,lpushx和rpushx命令特点是列表存在时才能加入。
# 示例
127.0.0.1:6379> lpushx mylist a
(integer) 4
127.0.0.1:6379> lpushx m d
(integer) 0
linsert命令:
linsert {key} {before|after} {pivot} {value} ,在列表中查找等于pivot的元素,并在其前(before)或后(after)插入元素value。返回值为列表中元素个数,失败时返回-1。
# 示例
127.0.0.1:6379> lpush mylist c b a
(integer) 3
# lrange key 0 -1为从左到右查询所有元素
127.0.0.1:6379> lrange mylist 0 -1
1) "a"
2) "b"
3) "c"
127.0.0.1:6379> linsert mylist before b z
(integer) 4
127.0.0.1:6379> linsert mylist after b y
(integer) 5
127.0.0.1:6379> lrange mylist 0 -1
1) "a"
2) "z"
3) "b"
4) "y"
5) "c"
# 向不存在的元素d后插入,会导致插入失败
linsert mylist after d e
(integer) -1
lrange命令:
lrange {key} {start} {end} ,获取指定范围的元素列表,start为开始的元素索引,end为结束的元素索引(end索引元素依然会取,这里可能和部分编程语言的一些内置函数不一样)。索引可以为负数,负数时为从list右边数起。给定的索引可以超出边界,redis底层会把超出边界的索引处理成边界索引。因为是从左向右获取的元素,如果start大于end的索引将取不到任何元素,返回(empty array)。
# 示例
127.0.0.1:6379> rpush uid 1 2 3 4 5
(integer) 5
127.0.0.1:6379> lrange uid 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
# start超出边界
127.0.0.1:6379> lrange uid -11 -1
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
# start(4-2=2)索引超过end(4-4=0)将取不到元素
127.0.0.1:6379> lrange uid -2 -4
(empty array)
//关于负数索引转换的源码,llen为列表长度(元素个数)
/* convert negative indexes */
if (start < 0) start = llen+start;
if (end < 0) end = llen+end;
if (start < 0) start = 0;
lindex命令:
lindex {key} {index} ,获取list指定索引的元素。索引可以为负,即从列表右端开始数起。
# 示例 测试列表为lrange所插入的uid
127.0.0.1:6379> lindex uid 2
"3"
127.0.0.1:6379> lindex uid -1
"5"
127.0.0.1:6379> lindex uid 7
(nil)
llen命令:
llen {key} ,返回指定list的长度(元素个数)。
# 示例
127.0.0.1:6379> lpush uid 1 2 3
(integer) 3
127.0.0.1:6379> llen uid
(integer) 3
# 对不存在的list使用llen
127.0.0.1:6379> llen non_exists_list
(integer) 0
lpop/rpop命令:
lpop/rpop {key} {count} ,从列表左端/右端弹出一个元素,count为弹出个数。返回元素的值,不存在返回nil。
# 示例
127.0.0.1:6379> rpush uid 1 2 3 4 5
(integer) 5
127.0.0.1:6379> lpop uid
"1"
127.0.0.1:6379> rpop uid
"5"
127.0.0.1:6379> lpop uid 3
1) "2"
2) "3"
3) "4"
127.0.0.1:6379> lpop non_exists_list
(nil)
lrem命令
lrem {key} {count} {value} ,lrem命令会从列表中找到值等于value的元素进行删除,根据count的不同有以下三种情况:
# 示例
127.0.0.1:6379> rpush uid 4 3 3 1 3 1 3 2 2 1 2
(integer) 11
# 从左到右删除2个元素3
127.0.0.1:6379> lrem uid 2 3
(integer) 2
127.0.0.1:6379> lrange uid 0 -1
1) "4"
2) "1"
3) "3"
4) "1"
5) "3"
6) "2"
7) "2"
8) "1"
9) "2"
# 从右到左删除1个元素2
127.0.0.1:6379> lrem uid -1 2
(integer) 1
127.0.0.1:6379> lrange uid 0 -1
1) "4"
2) "1"
3) "3"
4) "1"
5) "3"
6) "2"
7) "2"
8) "1"
# 删除所有元素1
127.0.0.1:6379> lrem uid 0 1
(integer) 3
127.0.0.1:6379> lrange uid 0 -1
1) "4"
2) "3"
3) "3"
4) "2"
5) "2"
ltrim命令:
ltrim {key} {start} {end} ,按照索引的范围修剪列表(并覆盖原列表),start为开始位置的索引,end为结束位置的索引。索引可以为负数,代表从右端开始数起。修剪是从左向右的,所以start需要大于end的索引,否则修剪出来会为空列表。修剪成功返回OK。
# 示例
127.0.0.1:6379> rpush uid 1 2 3 4 5 6
(integer) 6
127.0.0.1:6379> ltrim uid 1 3
OK
127.0.0.1:6379> lrange uid 0 -1
1) "2"
2) "3"
3) "4"
127.0.0.1:6379> ltrim uid -3 -2
OK
127.0.0.1:6379> lrange uid 0 -1
1) "2"
2) "3"
# 可以给不存在的key修剪
127.0.0.1:6379> ltrim a 1 1
OK
127.0.0.1:6379> lrange a 0 -1
(empty array)
lset命令:
lset {key} {index} {newValue} ,给指定key的列表中索引为index的元素修改值为newValue。索引可以为负数,代表从列表右端开始数起。若指定的key不存在返回ERR no such key,修改值成功则返回列表长度。
# 示例
127.0.0.1:6379> lset non_exists_key 1 1
(error) ERR no such key
127.0.0.1:6379> rpush uid 1 2 3
(integer) 3
127.0.0.1:6379> lrange uid 0 -1
1) "1"
2) "2"
3) "3"
127.0.0.1:6379> lset uid 0 2
OK
127.0.0.1:6379> lrange uid 0 -1
1) "2"
2) "2"
3) "3"
127.0.0.1:6379> lset uid -1 2
OK
127.0.0.1:6379> lrange uid 0 -1
1) "2"
2) "2"
3) "2"
blpop/brpop命令:
blpop/brpop {key1} {key2…} {timeout} ,brpop和blpop是rpop和lpop的阻塞版本。可以包含多个key,多个key都不为空时,从左到右优先取第一个不为空的key的元素,返回key名与元素。timeout为阻塞时最大等待时间(超时时间),单位秒。下面用多种示例来说明这两个命令:
# 示例
127.0.0.1:6379> rpush uid 1 2 3
(integer) 3
127.0.0.1:6379> rpush uid2 3 2 1
(integer) 3
# 优先弹出元素顺序(uid>uid2),timeout设为3s
127.0.0.1:6379> blpop uid uid2 3
1) "uid"
2) "1"
# 优先弹出元素顺序(uid2>uid),timeout设为3s
127.0.0.1:6379> brpop uid2 uid 3
1) "uid2"
2) "1"
# 将uid剩下两个元素弹出,令uid为空
127.0.0.1:6379> lpop uid 2
1) "2"
2) "3"
# uid为空,从uid2弹出元素
127.0.0.1:6379> blpop uid uid2 3
1) "uid2"
2) "3"
# uid为空,timeout设为3s,3s内该终端无法操作,3s后没有向uid插入元素则超时,返回nil与时间
127.0.0.1:6379> blpop uid 3
(nil)
(3.05s)
# uid为空,timeout设为30s,通过另一终端用lpush向uid插入元素"yes",该终端取到元素后弹出并返回key名,元素和时间。
127.0.0.1:6379> blpop uid 30
1) "uid"
2) "yes"
(12.86s)
# 以上的例子可以看到一个细节,就是用阻塞弹出命令时若list开始就不为空,返回值是不会带有时间的。
以上就是list的基本API,附一张《Redis开发与运维》的list命令时间复杂度图片。
以下是其余的list API介绍。
lmove命令:
lmove {source} {destination} {LEFT|RIGHT} {LEFT|RIGHT} ,lmove命令把source列表的左端或右端(第一个LEFT|RIGHT指定)元素弹出,并插入到destination列表的左端或右端(第二个LEFT|RIGHT指定)。返回值为source列表被弹出的元素。注意redis命令中使用大写还是小写都相同,大小写不需要区分。
# 示例
127.0.0.1:6379> rpush uid 1 2 3
(integer) 3
127.0.0.1:6379> rpush uname a b c
(integer) 3
# 把uid左端的"1"弹出并加入到uname的右端
127.0.0.1:6379> lmove uid uname left right
"1"
127.0.0.1:6379> lrange uid 0 -1
1) "2"
2) "3"
127.0.0.1:6379> lrange uname 0 -1
1) "a"
2) "b"
3) "c"
4) "1"
# 给一个不存在的key使用lmove插入元素,会把它创建并插入元素
127.0.0.1:6379> lmove uid unew left right
"2"
127.0.0.1:6379> lrange unew 0 -1
1) "2"
lmpop命令:
lmpop {numkeys} {key1} {key2…} {LEFT|RIGHT} (COUNT count) ,lmpop命令类似于普通的pop但是这里可以写上numkeys(keys的数量)以及后面写上等于该数量的key名。该命令会按输入的key从左至右的顺序查找第一个不为空的list,并对它进行元素的弹出。LEFT或RIGHT为选择从左端弹出还是右端弹出,COUNT为可选参数,后面跟上一个数字代表弹出元素的个数,缺省时默认为弹出一个元素。返回值为有元素被弹出的key以及被弹出的元素。
注意:虽然输入了多个key,但是lmpop只会对从左到右的第一个不为空的list进行弹出。然后如果写在{}里的是必须参数,()里的是可选参数。
# 示例
127.0.0.1:6379> rpush uid 1 2 3 4 5
(integer) 5
127.0.0.1:6379> rpush uname a b c d e
(integer) 5
# numkeys=2,后面跟2个key:uid和uname,left为左端弹出,count 2为弹出2个元素
127.0.0.1:6379> lmpop 2 uid uname left count 2
1) "uid"
2) 1) "1"
2) "2"
# COUNT参数缺省,弹出1个元素
127.0.0.1:6379> lmpop 1 uid left
1) "uid"
2) 1) "3"
# 对不存在的key使用lmpop返回nil
127.0.0.1:6379> lmpop 1 umo left
(nil)
# numkeys=3,与输入的key个数(2个)不一致,报语法错误
127.0.0.1:6379> lmpop 3 uid uname left
(error) ERR syntax error
# count参数可以大于list所剩元素个数,仅返回剩下的元素
127.0.0.1:6379> lrange uid 0 -1
1) "4"
2) "5"
127.0.0.1:6379> lmpop 1 uid left count 3
1) "uid"
2) 1) "4"
2) "5"
lpos命令
lpos {key} {element} (Rank rank) (COUNT num-matches) (MAXLEN len) ,lpos命令为获取所输入的元素在key指定的list的索引。Rank为可选参数,Rank后输入一个数字rank,rank为正数表示返回从左到右第rank个等于element的元素的索引,rank为负数时表示从右到左第rank个等于element的元素,缺省为1。
COUNT为可选参数,COUNT后输入一个数字num-matches,表示最多返回num-matches个等于element的元素。MAXLEN为可选参数,MAXLEN后输入一个数字len表示该命令从左到右扫描的最大长度。
# 示例
127.0.0.1:6379> lrange uid 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
127.0.0.1:6379> lpos uid 3
(integer) 2
# rank = 2 ,uid中没有第2个等于3的元素,所以返回nil
127.0.0.1:6379> lpos uid 3 rank 2
(nil)
# 在右端加入一个元素1,进行多个相同元素的测试
127.0.0.1:6379> rpush uid 1
(integer) 6
# count = 2,返回2个元素1的索引
127.0.0.1:6379> lpos uid 1 count 2
1) (integer) 0
2) (integer) 5
# rank = -1 ,意思为返回从右往左数的第一个元素1的索引
127.0.0.1:6379> lpos uid 1 rank -1
(integer) 5
# count = 2 ,返回2个元素1的索引,maxlen = 2 ,从左到右最多扫描2个元素
# 只扫描2个元素所以扫描不到最右端的1,只返回了第一个1的索引0
127.0.0.1:6379> lpos uid 1 count 2 maxlen 2
1) (integer) 0
blmove/blmpop命令:
blmove和blmpop命令是blmove和blmpop的阻塞版本,基本输入参数一样,都是多了个timeout(超时时间),单位为s,timeout为0时将会一直阻塞下去直到命令完成。
blmove {source} {destination} {LEFT|RIGHT} {LEFT|RIGHT} {timeout} ,这是blmove的参数。
blmpop {timeout} {numkeys} {key1…} {LEFT|RIGHT} (COUNT count) ,这是blmpop的参数。
# 示例
127.0.0.1:6379> rpush uid 1 2 3
(integer) 3
# uid不为空,可以直接执行
127.0.0.1:6379> blmove uid uid2 left left 3
"1"
# uid3为空,等待3s未有元素加入到uid3则超时退出命令
127.0.0.1:6379> blmove uid3 uid2 left left 3
(nil)
(3.11s)
# uid3为空,超时时间内另一个客户端往uid3 push一个元素1,返回被blmove的元素与完成时间
127.0.0.1:6379> blmove uid3 uid2 left left 5
"1"
(4.16s)
# uid不为空,可以直接执行
127.0.0.1:6379> blmpop 3 2 uid uid2 left count 2
1) "uid"
2) 1) "2"
2) "3"
# uid3,uid4都为空,等待3s未有元素加入到uid3和uid4则超时退出命令
127.0.0.1:6379> blmpop 3 2 uid3 uid4 left
(nil)
(3.02s)
# uid3,uid4都为空,超时时间内另一个客户端向uid4 push一个元素1,返回被弹出元素的
# key和被弹出的元素
127.0.0.1:6379> blmpop 5 2 uid3 uid4 left
1) "uid4"
2) 1) "1"
(3.34s)
3. list使用场景
1. 消息队列
Redis的lpush+brpop命令组合即可实现阻塞队列,生产者客户端使用lrpush从列表左侧插入元素,多个消费者客户端使用brpop命令 阻塞式的“抢”列表尾部的元素,多个客户端保证了消费的负载均衡和高可用性。
2. 文章列表
每个用户有属于自己的文章列表,现需要分页展示文章列表。此时可以考虑使用列表,因为列表不但是有序的,同时支持按照索引范围获取元素。
实际上列表的使用场景很多,在选择时可以参考以下口诀:
4. 内部编码
Redis3.2之后,quicklist已经取代了ziplist(压缩列表)和linkedlist(链表)作为list的内部编码。
这是网上的一篇关于quicklist的文章对quicklist的描述:快速列表(quicklist)是以压缩列表(ziplist)为节点的链表(linkedlist),将链表按段切分,每一段使用压缩列表进行内存的连续存储,多个压缩列表通过prev和next指针组成的双向链表。它结合了压缩列表和链表的优势,进一步压缩了内存的使用量,进一步提高了效率。
在Redis7.0版本,ziplist已经被listpack所取代,所以注意在Redis7.0中快速列表的每个结点不再是ziplist,而是listpack!也就是紧凑列表。
关于基本类型的内部编码在介绍API时均只简单介绍概念,在之后的文章会专门拿内部编码来进行详细探讨。
5. 源码分析环节
对于list,金毛站长还没发现什么奇怪的现象,有点可惜>﹏<。但是还是金毛站长还是会带着大家一起做源码分析的!本篇我们就来对list中最常使用的push和pop源码进行分析:
首先是push命令:
/* LPUSH <key> <element> [<element> ...] */
void lpushCommand(client *c) {
pushGenericCommand(c,LIST_HEAD,0);
}
/* RPUSH <key> <element> [<element> ...] */
void rpushCommand(client *c) {
pushGenericCommand(c,LIST_TAIL,0);
}
/* LPUSHX <key> <element> [<element> ...] */
void lpushxCommand(client *c) {
pushGenericCommand(c,LIST_HEAD,1);
}
/* RPUSH <key> <element> [<element> ...] */
void rpushxCommand(client *c) {
我们可以看到这4个push命令都调用了同一个函数:pushGenericCommand ,我们继续看看它的内部实现:
/* Implements LPUSH/RPUSH/LPUSHX/RPUSHX.
* 'xx': push if key exists. */
void pushGenericCommand(client *c, int where, int xx) {
int j;
//robj为redisObject,这里是用key到当前数据库查找对应的redisObject
robj *lobj = lookupKeyWrite(c->db, c->argv[1]);
//检查类型,如果lobj不等于OBJ_LIST(即不是list),checkType返回1,会触发return
if (checkType(c,lobj,OBJ_LIST)) return;
//key不存在
if (!lobj) {
//如果xx为true则指令是lpushx或rpushx,此时更新失败,直接返回
if (xx) {
addReply(c, shared.czero);
return;
}
//创建一个quicklist
lobj = createQuicklistObject();
//对lobj的quicklist进行一些初始化设置
quicklistSetOptions(lobj->ptr, server.list_max_listpack_size,
server.list_compress_depth);
//将key加入到当前数据库
dbAdd(c->db,c->argv[1],lobj);
}
//把所有输入的element从左或右端(由where参数指定)插入进lobj
for (j = 2; j < c->argc; j++) {
listTypePush(lobj,c->argv[j],where);
server.dirty++;
}
//向客户端回复一个long long型的数字(返回值),该数字为列表长度(元素个数)
addReplyLongLong(c, listTypeLength(lobj));
//以下代码是用于通知该key出现的更新
char *event = (where == LIST_HEAD) ? "lpush" : "rpush";
signalModifiedKey(c,c->db,c->argv[1]);
notifyKeyspaceEvent(NOTIFY_LIST,event,c->argv[1],c->db->id);
}
金毛站长对代码每个部分都做了中文注释,虽然其中调用的更底层的函数没有时间去介绍,但是你应该能看懂push命令的整个的流程了吧🍭🍭
如果你看懂了的话,接下来我们看pop命令:
/* LPOP <key> [count] */
void lpopCommand(client *c) {
popGenericCommand(c,LIST_HEAD);
}
/* RPOP <key> [count] */
void rpopCommand(client *c) {
popGenericCommand(c,LIST_TAIL);
}
我们看到lpop和rpop都调用了popGenericCommand这个函数,和上面的push很像,接下来让我们看看popGenericCommand的内部实现:
/* Implements the generic list pop operation for LPOP/RPOP.
* The where argument specifies which end of the list is operated on. An
* optional count may be provided as the third argument of the client's
* command. */
void popGenericCommand(client *c, int where) {
int hascount = (c->argc == 3);//如果参数个数等于3个,那么hascount = true
long count = 0;
robj *value;
//参数超过3个,报错
if (c->argc > 3) {
addReplyErrorArity(c);
return;
} else if (hascount) {
/* Parse the optional count argument. */
/* 将参数的count转换为long,赋值给count,成功返回C_OK*/
if (getPositiveLongFromObjectOrReply(c,c->argv[2],&count,NULL) != C_OK)
return;//未返回C_OK,退出
}
//在当前db中查找key对应的redisObject,并赋值给o
robj *o = lookupKeyWriteOrReply(c, c->argv[1], hascount ? shared.nullarray[c->resp]: shared.null[c->resp]);
//o为null或者o的类型不为list,return
if (o == NULL || checkType(c, o, OBJ_LIST))
return;
//如果输入了count且count为0,则直接返回空数组
if (hascount && !count) {
/* Fast exit path. */
addReply(c,shared.emptyarray);
return;
}
//这里count为0但hascount = false,是输入参数count缺省,默认弹出1个元素
if (!count) {
/* Pop a single element. This is POP's original behavior that replies
* with a bulk string. */
//弹出元素,检测错误,输出返回值到客户端,释放内存空间,检查是否要删除key
value = listTypePop(o,where);
serverAssert(value != NULL);
addReplyBulk(c,value);
decrRefCount(value);
listElementsRemoved(c,c->argv[1],where,o,1,NULL);
} else {
/* Pop a range of elements. An addition to the original POP command,
* which replies with a multi-bulk. */
/*这里是count有输入的时候,首先计算长度,范围长度,范围左端和右端,从
* 左端还是右端弹出(reverse)*/
long llen = listTypeLength(o);
long rangelen = (count > llen) ? llen : count;
long rangestart = (where == LIST_HEAD) ? 0 : -rangelen;
long rangeend = (where == LIST_HEAD) ? rangelen - 1 : -1;
int reverse = (where == LIST_HEAD) ? 0 : 1;
//输出返回值到客户端,删除元素,检查key长度是否为0而删除key
addListRangeReply(c,o,rangestart,rangeend,reverse);
listTypeDelRange(o,rangestart,rangelen);
listElementsRemoved(c,c->argv[1],where,o,rangelen,NULL);
}
}
以上就是pop的”浅层”源码,可以发现redis这一层上的源码还算好读的,一个指令的执行流程都可以看明白。等之后金毛站长开始研究内部编码时,会写博客来带大家一起详细读更底层的源码。可以持续关注金毛的个人网站哦!
6. 杂谈
还记得上篇金毛站长发现的hincrbyfloat奇怪现象吗?我对源代码做了改动并给redis提了pull request,也是人生第一次pr。
pr后紧张地等了三天,redis的maintainer之一终于来查看我的pr了。不过他对此没有意见,什么话都没说就把我的pr放到了Triage仓库里(他对Triage仓库的描述是这样的:Keep track of status of things that shouldn’t be forgotten),并且是移动到了Awaits merge(等待合并)清单中🍭🍭
金毛站长研究了一下这个仓库里的pr,一般都是影响不大的改动,合并后也只有很少一部分有必要在release notes上显示出来告知用户。我的这个小改动确实影响并不大,不修改也不会没法使用,修改了只是因为对AOF重启前后一致性上来说应该这么做,可以说影响小到大家甚至十年来都没有发现(#_<-)……最近看起来redis的maintainer们焦点正放在7.0下一个预发行版本RC3的新特性中,所以估计我的PR得等很久后才能被处理了,不过不管怎么样,被合并估计只是时间问题了。之前的博客都是基本API介绍,现在改成所有API介绍了,而且还是介绍最新版本的Redis。这也是金毛站长作为”准Redis contributor”对自身的要求🍭🍭
仔细想想,我可能是第一个在Redis7的基础上写教程博客的。如果你也喜欢学最新的Redis,那就持续关注金毛的小破站叭!🍭🍭