Aman Kumar Singh

Redis 列表是一个有序的字符串序列,你可以从任一端推入或弹出。这种简单结构使其成为两种常见需求的自然选择:生产者添加工作、消费者取走的队列,以及最近项目的上限 feed。列表并非适用于所有排队问题(streams 能处理高要求场景),但对于简单的 FIFO 工作和“最新 N”feed,它们是快速、正确的工具。

这是 Redis 大师班的第 5 部分,承接 hashes

推入与弹出

列表支持对两端的操作,左和右:

LPUSH queue:emails "job1"    # 添加到头部(左)
RPUSH queue:emails "job2"    # 添加到尾部(右)
LPOP queue:emails            # 从头部移除并返回
RPOP queue:emails            # 从尾部移除并返回
LLEN queue:emails            # 长度
LRANGE queue:emails 0 -1     # 所有元素

Enter fullscreen mode Exit fullscreen mode

将一端的 push 与另一端的 pop 结合,就得到了队列。LPUSHRPOP 是先入先出:最旧的项目先出来。LPUSHLPOP 是后进先出,即栈。这些操作都是原子的,因此多个生产者和消费者可以安全地操作同一个列表。

一个简单的工作队列

基本的工作队列是生产者 LPUSH 工作、工作者 RPOP 取出:

# 生产者
LPUSH jobs:email '{"to":"[email protected]","template":"welcome"}'

# 工作者
RPOP jobs:email    # 获取下一个任务,若为空则返回 nil

Enter fullscreen mode Exit fullscreen mode

普通 RPOP 的问题是空队列立即返回 nil,因此工作者必须循环轮询,消耗 CPU 并增加延迟。解决方案是阻塞变体:

BRPOP jobs:email 5    # 最多等待 5 秒获取任务,然后返回

Enter fullscreen mode Exit fullscreen mode

BRPOP 会阻塞直到有项目可用(或超时),因此工作者可以高效休眠,并在任务到达时立即唤醒。这将列表转变为真正的推送式工作队列,无需轮询。

可靠性差距

在使用列表作为任务队列前,需要了解一个诚实的限制。使用 RPOPBRPOP 时,一旦工作者弹出任务,它就从列表中消失了。如果该工作者在处理过程中崩溃,任务将丢失,因为它既不在队列中,也未完成。普通列表提供至多一次传递,这对于可以丢弃的工作可以接受,但对于不能丢弃的工作则不合适。

经典的缓解方法是 LMOVE(或旧的 RPOPLPUSH),它以原子方式从队列中弹出并推入“处理中”列表:

LMOVE jobs:email jobs:processing RIGHT LEFT

Enter fullscreen mode Exit fullscreen mode

现在任务在工作者处理时位于 jobs:processing。成功时,工作者从那里移除它;崩溃时,它仍在处理列表中,可以恢复。这种可靠队列模式弥补了丢失的差距,但它是手动簿记。当你需要真正的传递保证、消费者组和确认时,Redis streams(系列后续内容)正是为此而构建的,通常是重要工作的更好选择。使用列表进行简单或尽力而为的队列,使用 streams 进行可靠的队列。

上限 feed:“最新 N”模式

列表的另一个重要用途是最近项目的有界列表:最近 10 条通知、最近活动、时间线。你在前面 LPUSH 新项目,然后将列表修剪到固定长度,使其永不无限增长:

LPUSH user:1:notifications "You have a new follower"
LTRIM user:1:notifications 0 9    # 仅保留最新的 10 条

Enter fullscreen mode Exit fullscreen mode

LTRIM 保留指定范围并丢弃其余部分。在前面推送并修剪到前 N 项,你将得到一个自我维护的“最新 N”列表,可以通过 LRANGE user:1:notifications 0 -1 廉价地读取。这是一个干净的模式,适用于活动 feed 和最近项目小部件,你只需要显示最新的少数项目。

列表何时适合或不适合作为队列

为保持选择清晰:

  • 使用列表 进行简单的 FIFO/LIFO 队列、可以承受丢失的尽力而为的工作,以及上限的最近项目 feed。它最小且快速。
  • 使用 LMOVE 到处理列表,当你需要基本的崩溃恢复但不需要完整保证时。
  • 使用 streams,当你需要可靠传递、确认、多个消费者组或重放时。列表并非为此而构建,强行使用会导致脆弱的自定义簿记。

列表是直接的有序序列结构:从任一端推入和弹出,阻塞等待工作,修剪以限制长度。对于简单队列和最近项目 feed,它们正是正确的选择,了解其可靠性限制会告诉你何时转向 streams。

接下来,我们将介绍集合:具有快速成员测试和在 Redis 内完成的集合代数的无序唯一值集合。

关键要点

  • Redis 列表是一个有序序列,你可以从任一端推入和弹出;结合相反的两端可以得到 FIFO 队列。
  • 使用 BRPOP(阻塞弹出),使工作者高效等待任务,而不是轮询空队列。
  • 普通列表队列是至多一次:被工作者弹出的任务若随后崩溃则丢失。
  • LMOVE 到处理列表添加基本崩溃恢复;对于真正的传递保证,请使用 streams。
  • LPUSHLTRIM 廉价地维护上限的“最新 N”feed,适合通知和最近活动。

常见问题

如何使用 Redis 列表构建队列?

生产者 LPUSH 项目到列表,工作者从另一端 RPOP(或 BRPOP)以实现 FIFO 顺序。使用阻塞 BRPOP,使工作者高效等待新任务,而不是轮询空列表。

使用列表作为任务队列的问题是什么?

普通 RPOP/BRPOP 立即移除任务,因此如果工作者在处理过程中崩溃,任务将丢失(至多一次传递)。使用 LMOVE 到处理列表进行恢复,或使用 Redis streams 获得真正的传递保证。

BRPOP 做什么?

它是一个阻塞弹出:它等待直到列表上有项目可用或超时,然后返回它。这使工作者可以休眠直到工作到达,而不是重复轮询,提供推送式队列。

如何在列表中仅保留最新的 N 项?

使用 LPUSH 推送新项目,然后运行 LTRIM key 0 N-1 仅保留最新的 N 项,丢弃其余。这廉价地维护自我上限的最近项目 feed。

我应该使用列表还是 stream 进行排队?

使用列表进行简单或尽力而为的队列和最近项目 feed。当你需要可靠传递、确认、消费者组或重放时,使用 stream,这些列表无法在不进行脆弱的手动簿记的情况下提供。

进一步阅读


本文最初发布于 amanksingh.com/blog/redis-lists-queues

关于作者

Aman Kumar Singh 是印度诺伊达的一名团队负责人和高级软件工程师,撰写关于全栈工程、系统设计和使用 TypeScript、Next.js、NestJS、PostgreSQL 和 Redis 的生产 SaaS 的文章。