详细介绍了Redis的List类型的常见命令和应用方式。
Redis 的List实际上相当于Java 语言中的 LinkedList,即双向链表,这意味着Redis List支持常量时间插入和删除靠近头部和尾部的元素,即使插入了数百万个条目,时间复杂度为 O(1)。访问元素在列表的端点附近也非常快,但是如果尝试访问非常大的列表的中间元素,则速度很慢,因为它是 O(N) 操作。
Redis3.2之前在元素较少时实际上是采用ziplist结构,当链表entry数据超过512、或单个value 长度超过64,底层就会转化成linkedlist编码,而Redis3.2及其之后则采用quicklist结构代替ziplist和linkedlist。
Redis LIST的最大长度为 2^32 - 1
个元素(4294967295,每个链表超过 40 亿
个元素)。
Redis List只是字符串链表,按插入顺序排序,在将元素添加到 Redis List时,可以将新元素推送到列表的头部(左侧)或尾部(右侧)。
LPUSH/LPOP
命令在头部插入/取出一个新元素,而RPUSH/RPOP
在尾部插入/去除一个新元素。当对空键执行这两个操作之一时,将创建一个新链表。类似地,如果清空了链表,则将会从键空间中删除对应的key,即“key对应着一个空链表”和“不存在key”的语义是一样的,从其中获取值时返回的结果也一样,都是null。
127.0.0.1:6379> LPUSH a aa bb cc
(integer) 3
127.0.0.1:6379> lpop a
"cc"
127.0.0.1:6379> lpop a
"bb"
127.0.0.1:6379> lpop a
"aa"
127.0.0.1:6379> lpop a
(nil)
127.0.0.1:6379> EXISTS a
(integer) 0
LPUSH和RPUSH
命令的参数都可变,方便一次向LIST存入多个值,LPOP和RPOP后面也可以指定一个数值,这表示要一次性取出的元素的个数:
127.0.0.1:6379> LPUSH a qq ww ee
(integer) 3
127.0.0.1:6379> RPUSH a aa ss dd
(integer) 6
127.0.0.1:6379> LPOP a 2
1) "ee"
2) "ww"
127.0.0.1:6379> RPOP a 2
1) "dd"
2) "ss"
可以通过 LLEN
命令查看链表长度:
127.0.0.1:6379> LPUSH a aa bb cc
(integer) 3
127.0.0.1:6379> LLEN a
(integer) 3
LRANGE
命令可从list中从左到右获取一定范围的元素(不会删除元素),该命令需要两个索引参数,这是一定范围的第一个和最后一个元素(从左到右,包括两个端点)。0以及正数表示从头开始的索引,但这两个索引也都可以为负数,用来告知Redis从尾部开始计数,因此-1表示最后一个元素,-2表示list中的倒数第二个元素,以此类推。
127.0.0.1:6379> lpush a aa bb cc dd
(integer) 4
127.0.0.1:6379> LRANGE a 0 2
1) "dd"
2) "cc"
3) "bb"
127.0.0.1:6379> LRANGE a -1 -3
(empty array)
127.0.0.1:6379> LRANGE a -3 -1
1) "cc"
2) "bb"
3) "aa"
LRANGE命令可以非常快速的实现内存中的分页查询,性能非常高!
队列是先进先出的数据结构,常用于消息队列和异步逻辑处理,它会确保元素的访问顺序,可以使用List的LPUSH和RPOP
来实现队列:
127.0.0.1:6379> LPUSH a aa
(integer) 1
127.0.0.1:6379> LPUSH a bb
(integer) 2
127.0.0.1:6379> LPUSH a cc
(integer) 3
127.0.0.1:6379> RPOP a
"aa"
127.0.0.1:6379> RPOP a
"bb"
127.0.0.1:6379> RPOP a
"cc"
127.0.0.1:6379> RPOP a
(nil)
栈是先进后出的数据结构,跟队列正好相反,可以使用List的LPUSH和LPOP
来实现栈:
127.0.0.1:6379> LPUSH a aa
(integer) 1
127.0.0.1:6379> LPUSH a bb
(integer) 2
127.0.0.1:6379> LPUSH a cc
(integer) 3
127.0.0.1:6379> LPOP a
"cc"
127.0.0.1:6379> LPOP a
"bb"
127.0.0.1:6379> LPOP a
"aa"
127.0.0.1:6379> LPOP a
(nil)
某些情况下,我们可能并不需要将所有数据都存入List中,我们只想使用列表来存储最新的项目。
Redis 支持我们使用List作为上限集合,只记住最新的 N 项并使用 LTRIM
命令丢弃所有最旧的项。LTRIM的使用类似于LRANGE,但它把List从左边到右截取指定长度,并且丢弃剩下的元素。
这允许我们实现一个非常简单但有用的模式:一起做一个链表推送操作和一个链表修剪操作,以便添加一个新元素并丢弃超过限制的元素:
假设某个链表a只需要保存最新存入的三个元素,则可以在每次LPUSH
之后使用LTRIM
:
127.0.0.1:6379> LPUSH a aa bb cc dd
(integer) 4
127.0.0.1:6379> LTRIM a 0 2
OK
127.0.0.1:6379> LRANGE a 0 -1
1) "dd"
2) "cc"
3) "bb"
127.0.0.1:6379> LPUSH a qq ww rr tt
(integer) 7
127.0.0.1:6379> LTRIM a 0 2
OK
127.0.0.1:6379> LRANGE a 0 -1
1) "tt"
2) "rr"
3) "ww"
在使用LPUSH和RPOP实现队列时,有时链表是空的并且没有任何要处理的元素,所以 RPOP 只返回 NULL。在这种情况下,消费者将会被迫等待一段时间并使用 RPOP 重试。这称为轮询,在这种情况下不是一个好主意,因为它有几个缺点:
至2.0.0版本开始,Redis 实现了名为 BRPOP 和 BLPOP
的命令,它们是 RPOP 和 LPOP 的阻塞版本,如果链表为空,则能够阻塞这两个调用,仅当链表中添加新元素或达到用户指定的超时时间为时,它们才会返回给调用者。
127.0.0.1:6379> LPUSH a aa bb cc dd
(integer) 4
127.0.0.1:6379> BRPOP a 5
1) "a"
2) "aa"
上面的案例,表示使用BRPOP命令阻塞的从a队列获取元素,最多等待5秒。
BRPOP 和 BLPOP 的命令的注意点:
127.0.0.1:6379> LPUSH a aa bb cc
(integer) 3
127.0.0.1:6379> LPUSH b aa bb cc
(integer) 3
127.0.0.1:6379> BRPOP a b 4
1) "a"
2) "aa"
127.0.0.1:6379> BRPOP a b 4
1) "a"
2) "bb"
127.0.0.1:6379> DEL a
(integer) 1
127.0.0.1:6379> BRPOP a b 4
1) "b"
2) "aa"
127.0.0.1:6379> BRPOP a b 4
1) "b"
2) "bb"
127.0.0.1:6379> DEL b
(integer) 1
127.0.0.1:6379> BRPOP a b 4
(nil)
(4.08s)
至Redis 6.2.0开始,提供了LMOVE source destination LEFT|RIGHT LEFT|RIGHT
命令。该命令以原子方式删除存储在源中的列表的第一个/最后一个元素(头/尾取决于 wherefrom
参数),并将元素推送到存储的列表的第一个/最后一个元素(头/尾取决于 whereto
参数)。
127.0.0.1:6379> LPUSH a aa bb cc dd
(integer) 4
127.0.0.1:6379> LRANGE a 0 -1
1) "dd"
2) "cc"
3) "bb"
4) "aa"
127.0.0.1:6379> LPUSH b aa bb cc dd
(integer) 4
127.0.0.1:6379> LRANGE b 0 -1
1) "dd"
2) "cc"
3) "bb"
4) "aa"
127.0.0.1:6379> lmove a b left right
"dd"
127.0.0.1:6379> LRANGE b 0 -1
1) "dd"
2) "cc"
3) "bb"
4) "aa"
5) "dd"
127.0.0.1:6379> LRANGE a 0 -1
1) "cc"
2) "bb"
3) "aa"
返回被弹出和推送的元素,如果source不存在,则返回值null并且不执行任何操作。如果 source 和 destination 相同,则该操作相当于从列表中删除第一个/最后一个元素并将其作为列表的第一个/最后一个元素推送,因此可以认为它是一个列表旋转命令(或无操作)。
127.0.0.1:6379> LRANGE a 0 -1
1) "cc"
2) "bb"
3) "aa"
127.0.0.1:6379> LMOVE a a left right
"cc"
127.0.0.1:6379> LRANGE a 0 -1
1) "bb"
2) "aa"
3) "cc"
Redis 通常可以用作消息服务器来实现后台作业或其他类型的消息任务的处理。一种简单的队列形式通常是在生产者端将值LPUSH推入List中,并在消费者端使用RPOP(使用轮询)等待此值,如果客户端可以通过阻塞操作更好地服务,则使用 BRPOP。
然而,在这种情况下,获得的队列是不可靠的,因为消息可能会丢失,例如在出现网络问题的情况下,或者如果消费者在收到消息后还没有处理服务器就立即崩溃了。
LMOVE
(或用于阻塞变体的 BLMOVE)提供了一种避免此问题的方法:消费者获取消息并同时将其原子性的推送到另一个表示正在处理的List中,一旦消息被处理,将可以使用 LREM 命令从处理List中删除该消息。
注意如果使用这种模式,客户端可能会需要额外的监听处理List中停留时间过长的消息,并在需要时将这些超时的消息再次推送到队列中,或者手动清理。
使用具有相同源和目标key的 LMOVE,客户端可以在 O(N) 的时间复杂度中一个接一个地访问 N 元素列表的所有元素,而无需使用单个LRANGE操作将完整列表从服务器传输到客户端。
LMOVE
操作是原子性的,即使有多个客户端并行的轮换列表,它们也将获取不同的元素,直到访问了列表的所有元素。
以上特性使得实现一个必须由N个工作线程尽可能快地连续处理一组项目的系统变得非常简单。一个例子是一个监控系统,它必须检查一组网站是否都可以访问,这要求延迟尽可能小,因此可以使用多个并行工作线程。
以上这种实现具有简单的可扩展性和可靠性,因为即使消息丢失,该元素仍然在队列中,并将在下一次迭代中进行处理。
在此前的示例中,我们并没有在PUSH元素之前创建空List,或者在List不再有元素时删除空List。Redis负责在List为空时删除key,或者在key不存在时创建一个空List并且向其中添加给定的元素。
实际上这一特性不是特定于List,它适用于由多个元素组成的所有 Redis 数据类型——Streams, Sets, Sorted Sets 和Hashes。
基本上我们可以用三个规则来总结行为:
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> del a
(integer) 0
127.0.0.1:6379> LLEN a
(integer) 0
相关文章:
如有需要交流,或者文章有误,请直接留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!