WessonStar

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 指倒数第二个元素,依次类推。

LPUSHRPUSH 都是可变参数命令(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 具有特殊的列表命令,使这个用例更加可靠和高效

举个例子,两个流行的 Rubyresquesidekiq 在底层都使用 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),在该场景里用它并不是一个好主意,因为它有几个缺陷:

  1. 强制 Redis 和客户端处理无用的命令(当列表为空时,所有请求都不会完成实际工作,它们只会返回 NULL
  2. 对项目的处理过程添加了延迟。因为在 worker 收到了 NULL 之后,它会等待一段时间。为了使延迟更短,我们可以缩短 RPOP 调用之间的等待时间,进而放大了问题 1 ,即对 Redis 进行更多无用的 calls。

所以 Redis 实现了名为 BRPOPBLPOP 的命令,它们是 RPOPLPOP 的可阻塞版本:只有当新元素添加到列表中,或者超过了当用户指定的 timeout 的时候,它们才会返回到 caller 。

这是我们可以在 worker 中使用的 call BRPOP 的示例:

> brpop tasks 5
1) "tasks"
2) "do_something"

意思是:等待 tasks 列表中的元素,如果 5 秒后还没有元素可用就返回

Note: 您可以使用 0 作为 timeout 来一直等待元素。您还可以指定多个列表,以便同时等待多个列表,并在第一个列表收到元素时获得通知。

关于 BRPOP 的一些注意事项:

  1. 客户端以有序的方式接受服务:在其他客户端推入了一个元素时,阻塞等待列表的第一个客户端将首先接受服务
  2. RPOP 相比,返回值是不同的:它是一个双元素数组,它包含了键的名称(列表名)。这是因为 BRPOPBLPOP 能够阻塞等待来自多个列表的元素
  3. 如果到达了超时时间,则返回 NULL

关于列表和阻塞操作,您应该了解更多内容。我们建议您阅读以下内容:

  • 可以使用 RPOPLPUSH 构建更安全的队列或轮换队列
  • 还有一个 RPOPLPUSH 的阻塞变体,称为 BRPOPLPUSH

Automatic creation and removal of keys

到目前为止,在所有的示例中,我们都没有必要在推送元素之前创建空列表,或者删除不再包含元素的空列表。Redis 会帮我们做这些工作。

这个行为不是列表特有的,它适用于所有由多个元素组成的 Redis 数据类型 Stream, Set, SortedSetHashes

基本上,我们可以用三个规则来概括这一行为:

  1. 当我们向聚合数据类型添加元素时,如果目标键不存在,则在添加元素之前创建一个空的聚合数据类型。
  2. 当我们从聚合数据类型中删除元素时,如果值保持为空,就自动删除该键。 Stream 数据类型是此规则的唯一例外。
  3. 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)

相关文章: