Redis的数据类型详解和使用:List类型

详细介绍了Redis的List类型的常见命令和应用方式。

1 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命令可以非常快速的实现内存中的分页查询,性能非常高!

2 List实现队列

队列是先进先出的数据结构,常用于消息队列和异步逻辑处理,它会确保元素的访问顺序,可以使用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)

3 List实现栈

栈是先进后出的数据结构,跟队列正好相反,可以使用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)

4 上限链表

某些情况下,我们可能并不需要将所有数据都存入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"

5 阻塞队列

在使用LPUSH和RPOP实现队列时,有时链表是空的并且没有任何要处理的元素,所以 RPOP 只返回 NULL。在这种情况下,消费者将会被迫等待一段时间并使用 RPOP 重试。这称为轮询,在这种情况下不是一个好主意,因为它有几个缺点:

  1. 强制 Redis 和客户端处理无用的命令(链表为空时的所有请求将不会完成任何实际工作,它们只会返回 NULL)。
  2. 消息的处理增加延迟的风险,因为在消费者收到 NULL 后,通常会等待一段时间,然后再次发起LPOP。为了使延迟更小,我们可以在调用 RPOP 之间减少等待时间,但是放大了问题 1,即发起了更多无用的 Redis 调用。

至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 的命令的注意点:

  1. 可以使用 0 作为超时来表示永远的等待元素到来,也可以指定多个链表而不是一个,以便同时等待多个列表,并在某一个列表接收到元素时得到通知。
  2. 客户端以有序的方式提供服务:如果同时有多个链表被推送了元素,则第一个阻止等待的链表的元素最先返回,以此类推。
  3. 返回值与 RPOP和LPOP 不同:它是一个两个元素的数组,第一个元素是key的名称,第二个元素是对应的值,因为 BRPOP 和 BLPOP 能够阻止等待来自多个链表的元素,必须加以区分。
  4. 如果达到超时,则返回 NULL。
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)

6 元素原子移动

至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"

6.1 可靠队列

Redis 通常可以用作消息服务器来实现后台作业或其他类型的消息任务的处理。一种简单的队列形式通常是在生产者端将值LPUSH推入List中,并在消费者端使用RPOP(使用轮询)等待此值,如果客户端可以通过阻塞操作更好地服务,则使用 BRPOP。

然而,在这种情况下,获得的队列是不可靠的,因为消息可能会丢失,例如在出现网络问题的情况下,或者如果消费者在收到消息后还没有处理服务器就立即崩溃了。

LMOVE(或用于阻塞变体的 BLMOVE)提供了一种避免此问题的方法:消费者获取消息并同时将其原子性的推送到另一个表示正在处理的List中,一旦消息被处理,将可以使用 LREM 命令从处理List中删除该消息。

注意如果使用这种模式,客户端可能会需要额外的监听处理List中停留时间过长的消息,并在需要时将这些超时的消息再次推送到队列中,或者手动清理。

6.2 循环队列

使用具有相同源和目标key的 LMOVE,客户端可以在 O(N) 的时间复杂度中一个接一个地访问 N 元素列表的所有元素,而无需使用单个LRANGE操作将完整列表从服务器传输到客户端。

LMOVE操作是原子性的,即使有多个客户端并行的轮换列表,它们也将获取不同的元素,直到访问了列表的所有元素。

以上特性使得实现一个必须由N个工作线程尽可能快地连续处理一组项目的系统变得非常简单。一个例子是一个监控系统,它必须检查一组网站是否都可以访问,这要求延迟尽可能小,因此可以使用多个并行工作线程。

以上这种实现具有简单的可扩展性和可靠性,因为即使消息丢失,该元素仍然在队列中,并将在下一次迭代中进行处理。

7 自动创建和删除key

在此前的示例中,我们并没有在PUSH元素之前创建空List,或者在List不再有元素时删除空List。Redis负责在List为空时删除key,或者在key不存在时创建一个空List并且向其中添加给定的元素。

实际上这一特性不是特定于List,它适用于由多个元素组成的所有 Redis 数据类型——Streams, Sets, Sorted Sets 和Hashes。

基本上我们可以用三个规则来总结行为:

  1. 当我们向聚合数据类型添加元素时,如果目标key不存在,则会在添加元素之前创建一个空的聚合数据类型。
  2. 当我们从聚合数据类型中删除元素时,如果最终导致聚合类型为空(元素个数为0),则key会自动销毁。Stream 数据类型是此规则的唯一例外。
  3. 调用只读命令,例如 LLEN(返回列表的长度),或对于空key使用删除元素的写命令,总是产生相同的结果,就好像该key持有一个空的聚合类型一样。
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

相关文章:

  1. https://redis.io/topics/data-types
  2. https://redis.io/topics/data-types-intro

如有需要交流,或者文章有误,请直接留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

刘Java

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值