文档:列表

列表是字符串值的链表。列表常用于:

  • 实现栈和队列。
  • 为后台工作系统构建队列管理。

基本命令

  • LPUSH 将新元素添加到列表的头部;RPUSH 添加到尾部。
  • LPOP 从列表的头部移除并返回一个元素;RPOP 从列表的尾部执行相同的操作。
  • LLEN 返回列表的长度。
  • LMOVE 原子地将元素从一个列表移动到另一个列表。
  • LTRIM 将列表裁剪到指定的元素范围。

阻塞命令

列表支持多个阻塞命令。例如:

  • BLPOP 从列表的头部移除并返回一个元素。如果列表为空,命令将阻塞,直到元素可用或达到指定的超时。
  • BLMOVE 原子地将元素从源列表移动到目标列表。如果源列表为空,命令将阻塞,直到新元素可用。

请参阅完整的列表命令系列

示例

  • 将列表视为队列(先进先出)
127.0.0.1:6379> LPUSH bikes:repairs bike:1
(integer) 1
127.0.0.1:6379> LPUSH bikes:repairs bike:2
(integer) 2
127.0.0.1:6379> RPOP bikes:repairs
"bike:1"
127.0.0.1:6379> RPOP bikes:repairs
"bike:2"
  • 将列表视为栈(先进后出)
127.0.0.1:6379> LPUSH bikes:repairs bike:1
(integer) 1
127.0.0.1:6379> LPUSH bikes:repairs bike:2
(integer) 2
127.0.0.1:6379> LPOP bikes:repairs
"bike:2"
127.0.0.1:6379> LPOP bikes:repairs
"bike:1"
  • 检查列表的长度
127.0.0.1:6379> LLEN bikes:repairs
(integer) 0
  • 原子地从一个列表弹出元素并推送到另一个列表
127.0.0.1:6379> LPUSH bikes:repairs bike:1
(integer) 1
127.0.0.1:6379> LPUSH bikes:repairs bike:2
(integer) 2
127.0.0.1:6379> LMOVE bikes:repairs bikes:finished LEFT LEFT
"bike:2"
127.0.0.1:6379> LRANGE bikes:repairs 0 -1
1) "bike:1"
127.0.0.1:6379> LRANGE bikes:finished 0 -1
1) "bike:2"
  • 要限制列表的长度,可以使用 LTRIM
127.0.0.1:6379> RPUSH bikes:repairs bike:1 bike:2 bike:3 bike:4 bike:5
(integer) 5
127.0.0.1:6379> LTRIM bikes:repairs 0 2
OK
127.0.0.1:6379> LRANGE bikes:repairs 0 -1
1) "bike:1"
2) "bike:2"
3) "bike:3"

什么是列表?

要解释列表数据类型,最好从一点理论开始,因为“列表”这个词在信息技术人员中经常被不当使用。例如,“Python 列表”并非其名称可能暗示的(链表),而是数组(在 Ruby 中相同的数据类型实际上称为 Array)。

从一个非常普遍的角度来看,列表只是一系列有序的元素:10,20,1,2,3 是一个列表。但是使用数组实现的列表的属性与使用链表实现的列表的属性大相径庭。

列表是通过链表实现的。这意味着即使列表中有数百万个元素,在列表的头部或尾部添加新元素的操作也是在恒定时间内完成的。使用 LPUSH 命令将新元素添加到包含十个元素的列表头部的速度,与将元素添加到包含 1000 万个元素的列表头部的速度相同。

缺点是什么?在用数组实现的列表中,按索引访问元素非常快(恒定时间索引访问),而在用链表实现的列表中则不然(该操作所需的工作量与访问元素的索引成比例)。

列表用链表实现是因为对于数据库系统来说,能够以非常快的速度向非常长的列表添加元素至关重要。另一个强大的优势是,正如您稍后将看到的,列表可以在恒定时间内保持恒定长度。

当快速访问大量元素集合的中间部分很重要时,可以使用另一种不同的数据结构,称为有序集合。有序集合在有序集合教程页面中有所介绍。

列表入门

LPUSH 命令向列表的左侧(头部)添加新元素,而 RPUSH 命令向列表的右侧(尾部)添加新元素。最后,LRANGE 命令从列表中提取元素范围。

127.0.0.1:6379> RPUSH bikes:repairs bike:1
(integer) 1
127.0.0.1:6379> RPUSH bikes:repairs bike:2
(integer) 2
127.0.0.1:6379> LPUSH bikes:repairs bike:important_bike
(integer) 3
127.0.0.1:6379> LRANGE bikes:repairs 0 -1
1) "bike:important_bike"
2) "bike:1"
3) "bike:2"

请注意,LRANGE 接受两个索引,即要返回范围的第一个和最后一个元素。两个索引都可以是负数,这告诉 Valkey 从末尾开始计数:所以 -1 是最后一个元素,-2 是列表的倒数第二个元素,依此类推。

正如您所看到的,RPUSH 将元素追加到列表的右侧,而最后的 LPUSH 将元素追加到左侧。

这两个命令都是变长参数命令,这意味着您可以自由地在一次调用中将多个元素推送到列表中。

127.0.0.1:6379> RPUSH bikes:repairs bike:1 bike:2 bike:3
(integer) 3
127.0.0.1:6379> LPUSH bikes:repairs bike:important_bike bike:very_important_bike
127.0.0.1:6379> LRANGE bikes:repairs 0 -1
1) "bike:very_important_bike"
2) "bike:important_bike"
3) "bike:1"
4) "bike:2"
5) "bike:3"

列表上定义的一个重要操作是弹出元素的能力。弹出元素是同时从列表中检索元素并将其从列表中删除的操作。您可以从左侧和右侧弹出元素,类似于您可以将元素推入列表的两侧。我们将添加三个元素并弹出三个元素,因此在这一系列命令结束时,列表为空,并且没有更多元素可供弹出。

127.0.0.1:6379> RPUSH bikes:repairs bike:1 bike:2 bike:3
(integer) 3
127.0.0.1:6379> RPOP bikes:repairs
"bike:3"
127.0.0.1:6379> LPOP bikes:repairs
"bike:1"
127.0.0.1:6379> RPOP bikes:repairs
"bike:2"
127.0.0.1:6379> RPOP bikes:repairs
(nil)

Valkey 返回一个 NULL 值,表示列表中没有元素。

列表的常见用例

列表适用于许多任务,以下是两个非常有代表性的用例:

  • 记住用户在社交网络中发布的最新动态。
  • 使用生产者-消费者模式进行进程间通信,其中生产者将项目推入列表,消费者(通常是工作者)消费这些项目并执行操作。Valkey 具有特殊的列表命令,可以使此用例更可靠和高效。

例如,流行的 Ruby 库 resquesidekiq 都使用列表来实现后台作业。

流行的 Twitter 社交网络将用户发布的最新推文存储在列表中。

为了逐步描述一个常见的用例,假设您的主页显示了照片分享社交网络中发布的最新照片,并且您希望加快访问速度。

  • 每当用户发布新照片时,我们使用 LPUSH 将其 ID 添加到列表中。
  • 当用户访问主页时,我们使用 LRANGE 0 9 来获取最新的 10 个发布项目。

限制长度的列表

在许多用例中,我们只想使用列表来存储最新项目,无论是社交网络更新、日志还是其他任何内容。

Valkey 允许我们将列表用作有上限的集合,只记住最新的 N 个项目,并使用 LTRIM 命令丢弃所有最旧的项目。

LTRIM 命令与 LRANGE 类似,但它不是显示指定范围的元素,而是将此范围设置为新的列表值。给定范围之外的所有元素都将被删除。

例如,如果您正在将自行车添加到维修列表的末尾,但只想关注列表中停留时间最长的 3 辆自行车:

127.0.0.1:6379> RPUSH bikes:repairs bike:1 bike:2 bike:3 bike:4 bike:5
(integer) 5
127.0.0.1:6379> LTRIM bikes:repairs 0 2
OK
127.0.0.1:6379> LRANGE bikes:repairs 0 -1
1) "bike:1"
2) "bike:2"
3) "bike:3"

上面的 LTRIM 命令告诉 Valkey 只保留索引 0 到 2 的列表元素,其他所有都将被丢弃。这允许一个非常简单但有用的模式:同时执行列表推入操作 + 列表修剪操作,以添加新元素并丢弃超出限制的元素。使用带负索引的 LTRIM 可以只保留最新添加的 3 个元素:

127.0.0.1:6379> RPUSH bikes:repairs bike:1 bike:2 bike:3 bike:4 bike:5
(integer) 5
127.0.0.1:6379> LTRIM bikes:repairs -3 -1
OK
127.0.0.1:6379> LRANGE bikes:repairs 0 -1
1) "bike:3"
2) "bike:4"
3) "bike:5"

上述组合添加新元素并仅保留列表中最新的 3 个元素。使用 LRANGE 您可以访问顶部项目,而无需记住非常旧的数据。

注意:虽然 LRANGE 在技术上是 O(N) 命令,但访问列表头部或尾部的小范围是恒定时间操作。

列表上的阻塞操作

列表有一个特殊功能,使其适合实现队列,并通常作为进程间通信系统的构建块:阻塞操作。

假设您想用一个进程将项目推入列表,并使用不同的进程实际处理这些项目。这是常见的生产者/消费者设置,可以通过以下简单方式实现:

  • 为了将项目推入列表,生产者调用 LPUSH
  • 为了从列表中提取/处理项目,消费者调用 RPOP

然而,有时列表可能为空,没有可处理的项目,所以 RPOP 只返回 NULL。在这种情况下,消费者被迫等待一段时间,然后再次使用 RPOP 重试。这称为轮询,在这种情况下不是一个好主意,因为它有几个缺点:

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

因此,Valkey 实现了名为 BRPOPBLPOP 的命令,它们是 RPOPLPOP 的阻塞版本:如果列表为空,它们会阻塞,只有当新元素添加到列表或达到用户指定的超时时才返回给调用者。

这是我们可以在工作程序中使用的 BRPOP 调用的示例:

127.0.0.1:6379> RPUSH bikes:repairs bike:1 bike:2
(integer) 2
127.0.0.1:6379> BRPOP bikes:repairs 1
1) "bikes:repairs"
2) "bike:2"
127.0.0.1:6379> BRPOP bikes:repairs 1
1) "bikes:repairs"
2) "bike:1"
127.0.0.1:6379> BRPOP bikes:repairs 1
(nil)
(2.01s)

这意味着:“在列表 bikes:repairs 中等待元素,但如果在 1 秒后没有元素可用,则返回”。

请注意,您可以将超时设置为 0 以永久等待元素,您还可以指定多个列表而不仅仅是一个,以便同时等待多个列表,并在第一个列表接收到元素时得到通知。

关于 BRPOP 的几点注意事项:

  1. 客户端按顺序提供服务:第一个阻塞等待列表的客户端,在其他客户端推入元素时首先得到服务,依此类推。
  2. 返回值与 RPOP 不同:它是一个两元素的数组,因为它还包括键的名称,因为 BRPOPBLPOP 能够阻塞以等待来自多个列表的元素。
  3. 如果达到超时,则返回 NULL。

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

  • 可以使用 LMOVE 构建更安全的队列或循环队列。
  • 还有一个命令的阻塞变体,称为 BLMOVE

键的自动创建和删除

到目前为止,在我们的示例中,我们从不需要在推入元素之前创建空列表,或在列表不再包含元素时删除空列表。当列表为空时删除键,或当键不存在且我们尝试向其中添加元素时(例如,使用 LPUSH)创建空列表,这是 Valkey 的职责。

这并非列表特有,它适用于所有由多个元素组成的 Valkey 数据类型——流(Streams)、集合(Sets)、有序集合(Sorted Sets)和哈希(Hashes)。

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

  1. 当我们向聚合数据类型添加元素时,如果目标键不存在,则在添加元素之前会创建一个空的聚合数据类型。
  2. 当我们从聚合数据类型中移除元素时,如果值变为空,则键会自动销毁。流数据类型是此规则的唯一例外。
  3. 使用空键调用只读命令(如 LLEN,它返回列表的长度),或移除元素的写入命令,其结果总是与键持有所需类型的空聚合类型时相同。

规则 1 的示例

127.0.0.1:6379> DEL new_bikes
(integer) 0
127.0.0.1:6379> LPUSH new_bikes bike:1 bike:2 bike:3
(integer) 3

但是,如果键存在,我们不能对错误的类型执行操作:

127.0.0.1:6379> SET new_bikes bike:1
OK
127.0.0.1:6379> TYPE new_bikes
string
127.0.0.1:6379> LPUSH new_bikes bike:2 bike:3
(error) WRONGTYPE Operation against a key holding the wrong kind of value

规则 2 的示例

127.0.0.1:6379> RPUSH bikes:repairs bike:1 bike:2 bike:3
(integer) 3
127.0.0.1:6379> EXISTS bikes:repairs
(integer) 1
127.0.0.1:6379> LPOP bikes:repairs
"bike:3"
127.0.0.1:6379> LPOP bikes:repairs
"bike:2"
127.0.0.1:6379> LPOP bikes:repairs
"bike:1"
127.0.0.1:6379> EXISTS bikes:repairs
(integer) 0

所有元素弹出后,键将不再存在。

规则 3 的示例

127.0.0.1:6379> DEL bikes:repairs
(integer) 0
127.0.0.1:6379> LLEN bikes:repairs
(integer) 0
127.0.0.1:6379> LPOP bikes:repairs
(nil)

限制

列表的最大长度为 2^32 - 1 (4,294,967,295) 个元素。

性能

访问列表头部或尾部的列表操作是 O(1),这意味着它们效率很高。然而,操作列表中元素的命令通常是 O(n)。这些示例包括 LINDEXLINSERTLSET。在使用这些命令时请谨慎,尤其是在操作大型列表时。

替代方案

当您需要存储和处理不确定的事件序列时,请考虑使用流(Streams)作为列表的替代方案。