Redis 列表

Redis 列表简介

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

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

基本命令

  • LPUSH 向列表头部添加新元素;RPUSH 向尾部添加。
  • LPOP 从列表头部移除并返回元素;RPOP 执行相同操作,但从列表尾部移除。
  • LLEN 返回列表长度。
  • LMOVE 以原子方式将元素从一个列表移动到另一个列表。
  • LTRIM 将列表缩减到指定元素范围。

阻塞命令

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

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

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

示例

  • 将列表视为队列(先进先出)

  • 将列表视为栈(先进后出)

  • 检查列表长度

  • 原子性地从一个列表中弹出元素并推送到另一个列表

  • 要限制列表的长度,你可以调用 LTRIM

什么是列表?

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

从非常一般的角度来看,列表只是一系列有序元素:10、20、1、2、3 是一个列表。但是,使用数组实现的列表的属性与使用链表实现的列表的属性非常不同。

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

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

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

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

Redis 列表入门

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

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

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

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

Redis 列表中定义的一项重要操作是弹出元素的能力。弹出元素是同时从列表中检索元素并将其从列表中消除的操作。你可以从左侧和右侧弹出元素,类似于你可以将元素推送到列表两侧的方式。我们将添加三个元素并弹出三个元素,因此在此命令序列的末尾,列表将为空,并且没有更多元素可以弹出

Redis 返回一个 NULL 值来表示列表中没有元素。

列表的常见用例

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

  • 记住用户发布到社交网络中的最新更新。
  • 使用消费者-生产者模式的进程之间的通信,其中生产者将项目推送到列表中,而消费者(通常是工作者)消耗这些项目并执行操作。Redis 有特殊的列表命令,使此用例变得更加可靠和高效。

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

流行的 Twitter 社交网络 获取用户发布的最新推文,并将其放入 Redis 列表中。

为了逐步描述一个常见用例,想象一下你的主页显示了照片分享社交网络中发布的最新照片,并且你想加速访问。

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

有上限的列表

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

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

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

例如,如果你正在向维修列表的末尾添加自行车,但只想关注列表中最久的三辆自行车

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

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

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

列表上的阻塞操作

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

想象一下,你想使用一个进程将项目推送到列表中,并使用另一个进程对这些项目执行某种实际工作。这是通常的生产者/消费者设置,并且可以用以下简单方式实现

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

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

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

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

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

它的含义是:“等待列表 bikes:repairs 中的元素,但如果 1 秒后仍没有元素可用,则返回”。

请注意,你可以使用 0 作为超时时间,以便无限期地等待元素,你还可以指定多个列表,而不仅仅是一个列表,以便同时等待多个列表,并在第一个列表接收到元素时收到通知。

关于 BRPOP 需要注意的几点

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

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

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

自动创建和删除键

到目前为止,在我们的示例中,我们从未在推送元素之前创建空列表,或在列表中不再有元素时删除空列表。当列表为空时,Redis 负责删除键,或者如果键不存在且我们尝试向其中添加元素(例如,使用 LPUSH),则创建空列表。

这不仅仅适用于列表,它适用于所有由多个元素组成的 Redis 数据类型——流、集合、有序集合和哈希。

基本上,我们可以用三条规则总结这种行为

  1. 当我们向聚合数据类型添加元素时,如果目标键不存在,则在添加元素之前创建一个空的聚合数据类型。
  2. 当我们从聚合数据类型中删除元素时,如果值仍然为空,则该键会自动销毁。流数据类型是此规则的唯一例外。
  3. 使用空键调用只读命令(例如 LLEN(返回列表的长度))或删除元素的写命令,始终产生与键保存命令期望找到的类型的空聚合类型相同的结果。

规则 1 的示例

但是如果键存在,我们无法对错误类型执行操作

规则 2 的示例

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

规则 3 的示例

限制

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

性能

访问列表头部或尾部的列表操作为 O(1),这意味着它们非常高效。但是,操作列表中元素的命令通常为 O(n)。这些命令的示例包括 LINDEXLINSERTLSET。在运行这些命令时要小心,尤其是在操作大型列表时。

替代方案

当你需要存储和处理不确定的事件序列时,可以考虑使用 Redis 流 作为列表的替代方案。

了解更多

给此页面评分