Redis 列表

Redis 列表简介

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

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

基本命令

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

阻塞命令

列表支持几种阻塞命令。例如

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

参阅完整的列表命令系列

示例

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

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

  • 检查列表的长度

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

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

什么是列表?

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

从非常笼统的角度来看,列表只是有序元素的序列: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,但不是显示指定范围的元素,而是将此范围设置为新的列表值。给定范围之外的所有元素都将被移除。

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

上面的 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 会创建一个空列表。

这并非列表特有,它适用于所有由多个元素组成的 Redis 数据类型——流、集合、有序集合和散列。

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

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

规则 1 的示例

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

规则 2 的示例

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

规则 3 的示例

限制

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

性能

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

替代方案

当您需要存储和处理不确定系列事件时,可以考虑将Redis Streams 作为列表的替代方案。

了解更多

评价此页面
返回顶部 ↑