redis 数据结构之链表 List
为了解释 List 数据类型,最好从一点理论开始,因为术语 List 经常被信息技术人员以不正当的方式使用。例如,“Python Lists” 不是名称所暗示的(Linked Lists),而是Arrays(实际上相同的数据类型在 Ruby 中称为 Array)。
一般看来,List 只是一系列有序元素。例如:10,20,1,2,3 是一个 List。但是使用 Array 实现的 List 的属性与使用 Linked List 实现的 List 的属性有很大区别。
Redis 的 List 通过 Linked List 实现。这意味着即使List中有数百万个元素,在 List 的头部或尾部添加新元素也会在常量时间内。使用 LPUSH 命令将新元素添加到一个具有十个元素的 List 的头部的速度与将具有 1000 万个元素的元素添加到 List 头部的速度相同。
由链表实现的 List 有什么缺点?在使用Array(常量时间索引访问)实现的 List 中,通过索引访问元素非常快,而在由链表实现的 List 中则不是那么快(其中操作需要与所访问元素的索引成比例的工作量)。
Redis List 使用 Linked List 实现,因为对于数据库系统,能够以非常快的方式将元素添加到很长的 List 中是至关重要的。正如您稍后将看到的那样,Redis Lists 可以在恒定时间内保持恒定长度。
当需要快速访问大量元素集合的中间位置时,可以使用不同的数据结构,称为排序集(Sorted sets)。排序集将在本教程后面介绍。
1. 使用 Redis List的第一步
LPUSH 命令将新元素添加到左侧(头部)的 List 中,而 RPUSH 命令将新元素添加到右侧(尾部)的 List 中。最后,LRANGE 命令从List中提取元素范围:
> rpush mylist A
(integer) 1
> rpush mylist B
(integer) 2
> lpush mylist first
(integer) 3
> lrange mylist 0 -1 // 这里使用 0 -1 表示显示所有元素,注意是:0 空格 -1,0 代表第一个元素,-1 代表最后一个元素
1) "first"
2) "A"
3) "B"
请注意,LRANGE 需要两个索引,即要返回的范围的第一个和最后一个元素。两个索引都可以是负数,告诉 Redis 从结尾开始计数:所以 -1 是最后一个元素,-2 是 List 的倒数第二个元素,依此类推。
正如您所见,RPUSH 附加了 List 右侧的元素,而最后的 LPUSH 附加了左侧的元素。这两个命令都是可变参数命令,这意味着您可以在一次调用中将多个元素自由地推送到 List 中:
> rpush mylist 1 2 3 4 5 "foo bar"
(integer) 9
> lrange mylist 0 -1
1) "first"
2) "A"
3) "B"
4) "1"
5) "2"
6) "3"
7) "4"
8) "5"
9) "foo bar"
Redis List 中定义的一个重要操作是 POP 元素的能力。POP 元素是从 List 中检索元素并同时从 List 中删除元素的操作。您可以从左侧和右侧弹出元素,类似于如何在 List 的两侧推送元素:
> rpush mylist a b c
(integer) 3
> rpop mylist
"c"
> rpop mylist
"b"
> rpop mylist
"a"
我们添加了三个元素并弹出了三个元素,因此在这个命令序列的末尾,List 为空,并且没有更多元素可以弹出。如果我们尝试弹出另一个元素,这就是我们得到的结果:
> rpop mylist
(nil)
Redis 返回 nil 值以表示List中没有元素。
2. Redis List 的常见用例
Redis List 对于许多任务很有用,两个非常有代表性的用例如下:
- 记住用户发布到社交网络的最新更新。
- 流程之间的通信,使用生产者-消费者模式,生产者将项目推入 List,消费者消费这些项目。 Redis 具有特殊的 List 命令,使这个流程更加可靠和高效。
例如,Ruby 库的 resque 和 sidekiq 都使用 Redis List 来实现后台任务。Twitter 社交网络将用户发布的最新推文收录到 Redis List 中。
要逐步描述常见用例,请假设您的主页显示在照片是共享社交网络中发布的最新照片,并且您希望加快访问速度。
- 每次用户发布新照片时,我们都会将其 ID 添加到带有 LPUSH 的 List 中。
- 当用户访问主页时,我们使用
LRANGE 0 9
来获取最新的 10 张照片。
3. List 上限
在许多用例中,我们只想使用 List 来存储最新的项目,无论它们是什么:社交网络更新,日志或其他任何内容。Redis 允许我们使用 List 作为上限集合,只记住最新的 N 项并使用 LTRIM 命令丢弃所有最旧的项。
LTRIM 命令类似于 LRANGE,但它不是显示指定范围的元素,而是将此范围设置为新 List 值。超出给定范围之外的所有元素都删除掉。
来看一个例子:
> rpush mylist 1 2 3 4 5
(integer) 5
> ltrim mylist 0 2
OK
> lrange mylist 0 -1
1) "1"
2) "2"
3) "3"
上面的 LTRIM 命令告诉 Redis 只从索引 0 到 2 中获取 List 元素,其他所有内容都将被丢弃。这允许一个非常简单但有用的模式:PUSH 以添加新元素并丢弃超出限制的元素:
LPUSH mylist <some element>
LTRIM mylist 0 999
上面的组合添加了一个新元素,并且只将 1000 个最新元素放入 List 中。使用 LRANGE,您可以最新的数据,而无需记住非常旧的数据。
注意:虽然 LRANGE 在技术上是一个时间复杂度为 O(N) 命令,但是访问 List 的头部或尾部的小范围是一个恒定时间操作。
4. 阻止 List上的操作
List 具有使其适合实现 queues 的特殊功能,并且通常作为进程间通信系统的构建块:阻塞操作。想象一下,您希望将项目推送到具有一个流程的 List 中,并使用不同的流程来实际对这些项目进行某种工作。这是通常的生产者-消费者模型,可以通过以下简单方式实现:
- 将项目推入 List,生产者调用 LPUSH。
- 从 List 中提取/处理项目,消费者会调用 RPOP。
但是有时候 List 可能是空的并且没有任何东西可以处理,所以 RPOP 只返回 nil。在这种情况下,消费者被迫等待一段时间并再次使用 RPOP 重试。这称为轮询,在这种情况下不是一个好主意,因为它有几个缺点:
- 强制 Redis 服务端和客户端处理无用命令(当 List 为空时所有请求都不会完成实际工作,它们只会返回 nil)。
- 为项目处理添加延迟,因为在 worker 收到 nil 之后,它会等待一段时间。为了使延迟更小,我们可以在对 RPOP 的调用之间等待更少,从而放大问题 1,即对 Redis 进行更多无用的操作。
所以 Redis 实现了名为 BRPOP 和 BLPOP 的命令,它们是 RPOP 和 LPOP 的阻塞版本,如果 List 为空则阻塞:只有当新元素添加到 List 中时,或者当用户指定的超时时,它们才会返回调用者到达。
这是我们可以在 worker 中使用的 BRPOP 调用的示例:
> brpop tasks 5
1) "tasks"
2) "do_something"
这意味着:“等待 List 任务中的元素,但是如果 5 秒后没有元素可用则返回”。注意,您可以使用 0 作为超时来永久等待元素,并且您还可以指定多个 List 而不仅仅是一个,以便同时在多个 List 上等待,并在第一个 List 收到元素时收到通知。
关于BRPOP的一些注意事项:
- 客户端以有序的方式提供服务:阻止等待 List 的第一个客户端在某个其他客户端推送元素时首先提供服务,等等。
- 与 RPOP 相比,返回值是不同的:它是一个双元素数组,因为它还包含键的名称,因为 BRPOP 和 BLPOP 能够阻止等待来自多个 List 的元素。
- 如果超时,则返回 nil。
关于 List 和阻止操作,您应该了解更多内容。我们建议您阅读以下内容:
- 可以使用 RPOPLPUSH 构建更安全的队列或轮换队列。
- 还有一个命令的阻塞变体,称为 BRPOPLPUSH。
5. 自动创建和删除键值 key
到目前为止,在我们的示例中,我们从未必须在推送元素之前创建空 List ,或者在不再包含元素时删除空 List 。 Redis 负责在 List 为空时删除键值 key,或者如果键值 key 不存在则创建一个空 List ,并且我们尝试向其添加元素,例如,使用 LPUSH。
这不是列表特有的,它适用于由多个元素组成的所有 Redis 数据类型 : Streams,Sets,Sorted Sets 和 Hashes。
基本上我们可以用三个规则来概括行为:
- 当我们向聚合数据类型添加元素时,如果目标键值 key 不存在,则在添加元素之前会创建空聚合数据类型。
- 当我们从聚合数据类型中删除元素时,如果值保持为空,则会自动销毁该键值 key。 Stream 数据类型是此规则的唯一例外。
- 调用只读命令(如 LLEN(返回列表的长度))或写入命令删除元素(使用空键)始终产生相同的结果,命令期望找到,就好像键值 key 持有类型的空聚合类型一样。
- 规则 1 的例子:
> del mylist
(integer) 1
> lpush mylist 1 2 3
(integer) 3
但是,如果键值 key 存在,我们无法对错误的类型执行操作:
> set foo bar
OK
> lpush foo 1 2 3
(error) WRONGTYPE Operation against a key holding the wrong kind of value
> type foo
string
- 规则 2 的例子:
> lpush mylist 1 2 3
(integer) 3
> exists mylist
(integer) 1
> lpop mylist
"3"
> lpop mylist
"2"
> lpop mylist
"1"
> exists mylist
(integer) 0
弹出所有元素后,键值 key 不再存在。
- 规则 3 的例子:
> del mylist
(integer) 0
> llen mylist
(integer) 0
> lpop mylist
(nil)