Redis 类型之 List
Redis Lists
为了解释 List 数据类型,最好从一点理论开始,因为术语 List 经常被信息技术人员以不正当的方式使用。例如,Python 中的 Lists 不是名称所暗示的 Linked Lists ,而是 Arrays(实际上相同的数据类型在 Ruby 中被称为 Array )。
从非常一般的观点来看,List 只是一系列有序的元素:10, 20, 1, 2, 3 是一个列表。但是使用 Array 实现的 List 的属性与使用 Linked List 实现的 List 的属性非常不同。
Redis 列表通过 Linked Lists 实现。这意味着即使列表中有数百万个元素,也会在 常量时间 (constant time) 内在列表的头部或尾部添加新元素。使用 LPUSH
命令将新元素添加到具有十个元素的列表的头部的速度,与添加到具有1000万个元素的列表头部的速度是相同的。
有什么缺点呢?在使用 Array(常量时间索引访问)实现的列表中,通过 索引 (index) 访问元素非常快,而在链表实现的列表中则不是那么快(其中完成操作所需要的工作量与所访问元素的索引成正比)。
Redis Lists 使用链接列表实现,因为对于数据库系统来说,能够以非常快的方式将元素添加到很长的列表中是至关重要的。正如您稍后将看到的那样,Redis Lists 可以在恒定时间内保持恒定长度。
当快速访问大量元素集合的中间位置很重要时,可以使用不同的数据结构,称之为排序集(sorted sets)。排序集将在本教程后面介绍。
First steps with Redis Lists
LPUSH
命令将一个新元素添加到列表的左侧(头部),而 RPUSH
命令将一个新元素添加到列表的右侧(尾部)。最后,LRANGE
命令从列表中提取某个区间的元素。
> rpush mylist A
(integer) 1
> rpush mylist B
(integer) 2
> lpush mylist first
(integer) 3
> lrange mylist 0 -1
1) "first"
2) "A"
3) "B"
请注意, LRANGE
需要指定两个索引,即要返回的区间的第一个和最后一个元素位置。两个索引都可以是负数,Redis 会从结尾开始计数:-1 指最后一个元素,-2 指倒数第二个元素,依次类推。
LPUSH
和 RPUSH
都是可变参数命令(variadic commands),这意味着您可以在一次调用中将多个元素推送到列表中。
> 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 列表中定义的一个重要的操作是弹出元素 RPOP
(pop elements)。弹出元素会从列表中检索元素的同时,从列表中删除元素。就像推送元素一样,您也可以从列表的两侧弹出元素。
> rpush mylist a b c
(integer) 3
> rpop mylist
"c"
> rpop mylist
"b"
> rpop mylist
"a"
我们添加了三个元素并弹出了三个元素,因此现在列表是空的,没有更多元素可以被弹出了。如果我们尝试弹出一个元素,这就是我们得到的结果:
> rpop mylist
(nil)
Redis 返回一个 NULL
值来表示列表中没有元素。
Common use cases for lists
列表对许多任务都很有用,两个非常有代表性的用例如下:
- 记住用户发布到社交网络的最近更新
- 使用 consumer-producer 模式实现进程之间的通信,生产者把项目推入列表,消费者消费这些项目并执行操作。Redis 具有特殊的列表命令,使这个用例更加可靠和高效
举个例子,两个流行的 Ruby 库 resque 和 sidekiq 在底层都使用 Redis 列表来实现后台作业。
流行的 Twitter 社交网络将用户发布的最新推文收录到 Redis 列表中。
讲的更详细一点,假设您的主页会展示在照片共享社交网络中发布的最新照片,并且您希望加快访问速度。
- 用户每次发布新照片时,我们都会使用
LPUSH
将其 ID 添加到列表中 - 当用户访问主页时,我们使用
LRANGE 0 9
来获取最新的 10 张照片
Capped lists
在许多用例中,我们只想使用列表来存储最新的项目,而不管它们是什么:社交网络更新,日志或其他内容。
Redis 允许我们将列表用作一个带上限的集合,只记住最新的 N 项并使用 LTRIM
命令丢弃最旧的项。
LTRIM
类似于 LRANGE
,但它不是显示指定区间的元素,而是将此区间设置为新列表的值,除此之外的元素都会被移除。
举个例子:
> 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 之间的元素,丢弃其他的元素。这允许一个非常简单却很有用的模式:对列表执行一个 push 操作和一个 trim 操作,来添加新元素并丢弃超出限制的元素。
LPUSH mylist <some element>
LTRIM mylist 0 999
上面的组合添加了一个新元素,并且只将 1000 个最新的元素放入列表中。使用 LRANGE
你可以访问列表顶部元素而无需记住非常久的数据。
Note: 虽然
LRANGE
在技术上是一个 O(N) 命令,但是访问列表头部或尾部的小范围内的元素是一个常量时间操作。
Blocking operations on lists
列表有一个适合用来实现队列(queue)的特殊功能,并且通常作为进程间通信系统的构建块:阻塞操作。
想象一下,您希望在一个进程中将某些项推入一个列表,并在不同的进程中对这些项执行某种工作。这就是普通的生产者/消费者模型,可以通过以下简单的方式实现:
- 生产者 call
LPUSH
将项目推入列表 - 消费者 call
RPOP
从列表中提取/处理项目
然而有时候列表可能是空的,没有任何东西可以处理,所以 RPOP
只返回 NULL
。在这种情况下,消费者被迫等待一段时间并使用 RPOP
重试。这种方式叫做轮询(polling),在该场景里用它并不是一个好主意,因为它有几个缺陷:
- 强制 Redis 和客户端处理无用的命令(当列表为空时,所有请求都不会完成实际工作,它们只会返回
NULL
) - 对项目的处理过程添加了延迟。因为在 worker 收到了
NULL
之后,它会等待一段时间。为了使延迟更短,我们可以缩短RPOP
调用之间的等待时间,进而放大了问题 1 ,即对 Redis 进行更多无用的 calls。
所以 Redis 实现了名为 BRPOP
和 BLPOP
的命令,它们是 RPOP
和 LPOP
的可阻塞版本:只有当新元素添加到列表中,或者超过了当用户指定的 timeout 的时候,它们才会返回到 caller 。
这是我们可以在 worker 中使用的 call BRPOP
的示例:
> brpop tasks 5
1) "tasks"
2) "do_something"
意思是:等待 tasks 列表中的元素,如果 5 秒后还没有元素可用就返回
Note: 您可以使用 0 作为 timeout 来一直等待元素。您还可以指定多个列表,以便同时等待多个列表,并在第一个列表收到元素时获得通知。
关于 BRPOP
的一些注意事项:
- 客户端以有序的方式接受服务:在其他客户端推入了一个元素时,阻塞等待列表的第一个客户端将首先接受服务
- 与
RPOP
相比,返回值是不同的:它是一个双元素数组,它包含了键的名称(列表名)。这是因为BRPOP
和BLPOP
能够阻塞等待来自多个列表的元素 - 如果到达了超时时间,则返回
NULL
关于列表和阻塞操作,您应该了解更多内容。我们建议您阅读以下内容:
- 可以使用
RPOPLPUSH
构建更安全的队列或轮换队列 - 还有一个
RPOPLPUSH
的阻塞变体,称为BRPOPLPUSH
Automatic creation and removal of keys
到目前为止,在所有的示例中,我们都没有必要在推送元素之前创建空列表,或者删除不再包含元素的空列表。Redis 会帮我们做这些工作。
这个行为不是列表特有的,它适用于所有由多个元素组成的 Redis 数据类型 Stream
, Set
, SortedSet
和 Hashes
基本上,我们可以用三个规则来概括这一行为:
- 当我们向聚合数据类型添加元素时,如果目标键不存在,则在添加元素之前创建一个空的聚合数据类型。
- 当我们从聚合数据类型中删除元素时,如果值保持为空,就自动删除该键。
Stream
数据类型是此规则的唯一例外。 - calling 只读命令(如
LLEN
返回列表的长度)或者 calling 写入命令删除元素(使用空键)始终产生相同的结果,就好像键持有一个空聚合类型一样。
规则 1 示例:
> del mylist
(integer) 1
> lpush mylist 1 2 3
(integer) 3
但是,如果键存在,我们无法对错误的类型执行操作。
> 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
弹出所有的元素之后,键不再存在。
规则 3 示例:
> del mylist
(integer) 0
> llen mylist
(integer) 0
> lpop mylist
(nil)