上次我们说了Redis的字符串,以及它的数据结构SDS。今天,我们来看看列表List。
1,简介
Redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素(或者叫节点)到列表的头部(左边)或者尾部(右边)。左头右尾,你可以想象第一个元素,下标是0,在左边的头部,后面的元素不断追加到右边的尾部。一个列表最多可以包含 2^32 - 1 个元素 (4294967295, 每个列表超过40亿个元素)。这些元素依照进入的下标顺序来排序。
2,应用场景
一个key存储多个元素,可用来实现栈,队列,有限集合等。
3,语法
127.0.0.1:6379> LPUSH a 0 1 2 3 //往列表左边的头部依次压入0 1 2 3四个元素
(integer) 4
127.0.0.1:6379> LRANGE a 0 -1 //从列表中获取第1个到最后1个的值
1) "3"
2) "2"
3) "1"
4) "0"
127.0.0.1:6379> RPUSH a 'a' 'b' 'c' 'd' //往列表右边的尾部依次压入'a' 'b' 'c' 'd'四个元素
(integer) 8
127.0.0.1:6379> LRANGE a 0 -1
1) "3"
2) "2"
3) "1"
4) "0"
5) "a"
6) "b"
7) "c"
8) "d"
4,命令及相关场景
4.1 PUSH操作
4.1.1 RPUSH key value [value ...]
从列表的右边入列一个元素或多个元素,复杂度O(1)。
将所有指定的值插入存于key的列表的尾部(从右侧插入)。如果key不存在,那么PUSH之前,会先自动创建一个空的列表。如果key对应的值不是一个list的话,则会返回一个错误。如果同时push多个值的话,值会依次从左到右PUSH从尾部进入list。
PUSH和POP操作,其实是队列的基本操作。Redis的list可以成为一个强大的队列系统。我们在哪些地方会用到队列呢?下面,我们说两个例子:
4.1.1.1 顺序结构
由于队列有天然的入队先后顺序,所以可以用它来做一些按时间排序的操作。例如,发布微博评论,以微博ID为key,评论按评论时间入列,查看的时候,可以按时间顺序或倒序进行展示。每提交一次评论,都向list的末尾添加一个新的节点。
4.1.1.2 并行转串行
高并发场景下,后台的程序不可能立刻响应每一个用户的请求,尤其是请求特别占资源的服务的时候,我们就需要一个排队系统。根据用户的请求时间,将用户的请求放入队列中,后台程序依次从队列中获取任务,处理并将结果返回到结果队列。这其实也是一个生产者消费者模型。通过队列,我们将并行的请求转换成串行的任务队列,之后依次处理(当然后台的程序也可以多核并行处理)。
4.1.2 LPUSH key value [value ...]
从队列的左边入队一个或多个元素,复杂度O(1)。这个指令和RPUSH几乎一样,只是插入节点的方向相反了,是从list的头部(左侧)进行插入的。
4.1.3 RPUSH key value
从队列的右边入队一个元素,仅队列存在时有效。当队列不存在时,不进行任何操作。
127.0.0.1:6379> RPUSH a 0 1 2 3
(integer) 4
127.0.0.1:6379> DEL a
(integer) 1
127.0.0.1:6379> RPUSHX a 0
(integer) 0
4.1.4 LPUSHX key value
从队列的左边入队一个元素,仅队列存在时有效。当队列不存在时,不进行任何操作。
4.2 POP操作
PUSH操作,是从队列头部和尾部增加节点的操作。而POP是获取并删除头尾节点的操作。
4.2.1 LPOP key
从队列的左边出队一个元素,复杂度O(1)。如果list为空,则返回nil。
127.0.0.1:6379> LRANGE a 0 -1
1) "3"
2) "2"
3) "1"
4) "0"
5) "a"
6) "b"
7) "c"
8) "d"
127.0.0.1:6379> RPOP a
"d"
127.0.0.1:6379> LRANGE a 0 -1
1) "3"
2) "2"
3) "1"
4) "0"
5) "a"
6) "b"
7) "c"
4.2.2 RPOP key
从队列的右边出队一个元素,复杂度O(1)。如果list为空,则返回nil。见LPOP。
4.2.3 BLPOP key [key ...] timeout
在阻塞的 timeout 秒内,获得该列表中的第一个元素,并从列表中删除。如果超过 timeout 秒 时间,则返回nil。当 timeout 设为 0 时,表示永远阻塞。
这是LPOP的阻塞版本。在LPOP的时候,如果队列中没有值,则会立即返回一个nil。而BLPOP则会等待一段时间,如果list中有值(等待的时候,被添加的),则返回对应值;如果在给定时间内仍没有得到结果,则返回nil。
要注意的是,LBPOP 支持多个 key,也就是说可以同时监听多个 list,并按照 key 的顺序,依次检查 list 是否为空,如果不为空,则返回最优先的 list 中的值。如果都为空,则阻塞,直到有一个 list 不为空,那么返回这个 list 对应的值,如果到了阻塞时间,仍然没有一个列表可返回数据,则会返回nil的多组合值(multi-bulk value) 。如果更具体的介绍可以查看中文官网的文档:http://redis.cn/commands/blpop.html
127.0.0.1:6379> RPUSH a 0
(integer) 1
127.0.0.1:6379> RPUSH b 1
(integer) 1
127.0.0.1:6379> RPUSH c 2
(integer) 1
127.0.0.1:6379> LPOP a
"0"
127.0.0.1:6379> LPOP b
"1"
127.0.0.1:6379> LPOP c
"2"
127.0.0.1:6379> BLPOP a b c 1
(nil)
(1.09s)
4.2.4 BRPOP key [key ...] timeout
在阻塞的 timeout 秒内,获得该列表中的最后一个元素,并从列表中删除。如果超过 timeout 秒 时间,则返回nil。
参考 BLPOP。
更多关于阻塞的细节,可参考 https://redis.io/commands/BLPOP http://www.imooc.com/article/257718
4.3 POP and PUSH
4.3.1 RPOPLPUSH source destination
这个命令可以原子性地返回并删除 source 对应的列表的最后一个元素(尾部元素),并将该元素放入 destination 对应的列表的第一个元素位置(列表头部)。此命令返回被POP出来的元素。
127.0.0.1:6379> RPUSH a 0 1 2 3 4
(integer) 5
127.0.0.1:6379> LRANGE a 0 -1
1) "0"
2) "1"
3) "2"
4) "3"
5) "4"
127.0.0.1:6379> RPOPLPUSH a b
"4"
127.0.0.1:6379> RPOPLPUSH a b
"3"
127.0.0.1:6379> LRANGE a 0 -1
1) "0"
2) "1"
3) "2"
127.0.0.1:6379> LRANGE b 0 -1
1) "3"
2) "4"
这个有意思的命令有什么实际的用处呢?
4.3.1.1 安全的队列
Redis 通常都被用做一个处理各种后台工作或消息任务的消息服务器。一个简单的队列模式就是:生产者把消息放入一个列表中,等待消息的消费者用 RPOP 命令(用轮询方式),或者用 BRPOP 命令(如果客户端使用阻塞操作会更好)来得到这个消息。然而,因为消息有可能会丢失,所以这种队列并是不安全的。例如,当接收到消息后,出现了网络问题或者消费者端崩溃了,那么这个消息就丢失了。
RPOPLPUSH (或者其阻塞版本的 BRPOPLPUSH)提供了一种方法来避免这个问题:消费者端取到消息的同时把该消息放入一个正在处理中的列表。 当消息被处理了之后,该命令会使用 LREM 命令来移除正在处理中列表中的对应消息。
另外,可以添加一个客户端来监控这个正在处理中列表,如果有某些消息已经在这个列表中存在很长时间了(即超过一定的处理时限),那么这个客户端会把这些超时消息重新加入到队列中。
4.3.1.2 循环列表
RPOPLPUSH 命令的 source 和 destination 是相同的话,那么客户端在访问一个拥有 n 个元素的列表时,可以在 O (N) 时间里一个接一个获取列表元素,而不用像 LRANGE 那样需要把整个列表从服务器端传送到客户端。
上面这种模式即使在以下两种情况下照样能很好地工作:
- 有多个客户端同时对同一个列表进行旋转(rotating):它们会取得不同的元素,直到列表里所有元素都被访问过,又从头开始这个操作。
- 有其他客户端在往列表末端加入新的元素。
这个模式让我们可以很容易地实现这样一个系统:有 N 个客户端,需要连续不断地对一批元素进行处理,而且处理的过程必须尽可能地快。 一个典型的例子就是服务器上的监控程序:它们需要在尽可能短的时间内,并行地检查一批网站,确保它们的可访问性。
值得注意的是,使用这个模式的客户端是易于扩展(scalable)且安全的(reliable),因为即使客户端把接收到的消息丢失了, 这个消息依然存在于队列中,等下次迭代到它的时候,由其他客户端进行处理。
4.3.2 BRPOPLPUSH source destination timeout
弹出一个列表的值,将它推到另一个列表,并返回它;或阻塞,直到有一个可用。
RPOPLPUSH 的阻塞版本。timeout 的单位是秒,当 timeout 为 0 的时候,表示无限期阻塞。
具体使用参考 RPOPLPUSH。
4.4 其他
4.4.1 LLEN key
获得队列 (List) 的长度
4.4.2 LRANGE key start stop
从列表中获取指定返回的元素,范围用 start 和 stop 表示,都支持负数。负数表示从右向左数。需要注意的是,超出范围的下标不会产生错误:如果 start>stop,会得到空列表,如果 stop 超过队尾,则 Redis 会返回到队尾的最后一个元素。这个可以使用需要分页截取的场景。
4.4.3 LINDEX key index
通过列表索引获取一个元素。这个操作是指定位置来进行的,每次操作,list 都得找到对应的位置,因此算法复杂度为 O (N)。list 的下表是从 0 开始的,index 为负的时候是从右向左数。-1 表示最后一个元素。当下标超出的时候,会返回 nul。所以不用像操作数组一样担心范围越界的情况。
4.4.4 LSET key index value
设置队列里面一个元素的值。这个操作和 LINDEX 类似,只不过 LINDEX 是读,而 LSET 是写。下标的用法和 LINDX 相同。不过当 index 越界的时候,这里会报异常。
4.4.5 LREM key count value
从列表中删除元素。该命令用于从 key 对应的 list 中,移除前 count 次出现 的值为 value 的元素。此命令返回被移除的个数。
count 参数有三种情况:
count > 0: 表示从头向尾(左到右)移除值为 value 的元素。
count < 0: 表示从尾向头(右向左)移除值为 value 的元素。
count = 0: 表示移除所有值为 value 的元素。
例如,LREM list -2 "hello"
会从list
尾到头移除最后两处值为"hello"
的元素。注意,不存在的key将被当做空列表,当key不存在时,此命令返回0。
4.4.6 LTRIM key start stop
将列表按范围进行剪切,只保留范围内的元素。
这个命令和 LRANGE 十分相似,LRANGE 会将指定范围的元素返回给客户端,而 LTRIM 会对 list 进行修剪,使其只包含指定范围的元素。start 和 stop 表示范围,都支持负数。超出范围的下标不会产生错误:如果 start>stop,会得到空列表,如果 stop 超过队尾,则 Redis 会保留到队尾的最后一个元素。
4.4.7 LINSERT key BEFORE|AFTER pivot value
在列表中的另一个元素之前或之后插入一个元素。该命令将 value 插入值 key 对应的列表的基准值 pivot 的前面或是后面。
当 key 不存在时,这个 list 被视为空列表,任何操作都不会发生。
当 key 存在,但保存的不是 list,则会报 error。
该命令会返回修改之后的 list 的长度,如果找不到 pivot,则会返回 - 1。