Mobile wallpaper 1Mobile wallpaper 2Mobile wallpaper 3Mobile wallpaper 4
1904 字
10 分钟
Redis学习系列 | 先入先出?还能后进先出?

两个方向,四种组合#

List 是 Redis 里最容易让人”以为自己会了”的类型。打开文档一看:LPUSHRPUSHLPOPRPOP,就四个命令嘛,左右推推。合上文档开始写代码,写到一半发现不太确定:“等等,LPUSH + RPOP 是队列还是栈来着?”

这个困惑的根源是——List 同时能做队列和栈,但方向选择不一样,容易搞混。

先把基础操作理清楚:

# 从左侧推入
LPUSH mylist "a" "b" "c"
# mylist: [c, b, a] ← 注意顺序!最后推的 "c" 在最左边
# 从右侧推入
RPUSH mylist "x" "y"
# mylist: [c, b, a, x, y]
# 从左侧弹出
LPOP mylist # → "c"(最左边的)
# 从右侧弹出
RPOP mylist # → "y"(最右边的)

LPUSH 有个容易踩的小坑:它把最后一个参数放在最左。LPUSH mylist "a" "b" "c" 的结果是 [c, b, a],不是 [a, b, c]。想要保持顺序,用 RPUSH mylist "a" "b" "c"

四个命令排列组合,搞出两种经典模式:

模式推入弹出效果
队列 (FIFO)LPUSHRPOP先进先出,像排队
栈 (LIFO)LPUSHLPOP先进后出,像叠盘子

也可以反着来——RPUSH + LPOP 同样是队列。社区习惯用 LPUSH + RPOP,因为直觉上”从左边推进去,从右边流出来”,好记。

这个模型不复杂,看图就清楚#

动画里上半部分是队列模式(LPUSH + RPOP):蓝色元素从左进、从右出,先进来的先出去。下半部分是栈模式(LPUSH + LPOP):绿色元素同侧进出,后进来的反而先出去。

看完这张图,以后再也不会搞混了。

阻塞弹出:List 最实用的功能#

List 最妙的设计不是双向,是 阻塞弹出 —— BLPOPBRPOP

普通的 RPOP 在队列为空时返回 nil(空)。你只能写成死循环:

while True:
task = redis.rpop("queue")
if task:
process(task)
else:
time.sleep(0.1) # 空转,又慢又浪费 CPU

两个问题:一是 0.1 秒的延迟(消息来了要等最多 100ms 才被处理),二是 Redis 被无效请求轰炸。

BRPOP 解决的就是这个:

# 队列为空就挂起,有数据了立即返回
task = redis.brpop("queue", timeout=5)
if task:
process(task[1])

timeout=5 的意思是:如果 5 秒内没数据,返回 None。设为 0 就无限等。拿到数据后几乎零延迟。

一个细节值得注意: BRPOP 返回的是一个元组 (key, value),不是直接返回值。因为 BRPOP 可以同时监听多个队列:

# 优先消费 high 队列,没数据才看 low 队列
BRPOP high_queue low_queue 5

这在业务上很实用——比如 VIP 用户的邮件优先发送、付费任务优先处理。Redis 按参数顺序检查,哪个队列先有数据就返回哪个。

当消息队列用:够用但要知道边界#

List + BRPOP 组合是一个轻量级消息队列。部署简单,不需要额外组件,适合很多场景。

生产者:

def produce(task_json):
redis.lpush("task_queue", task_json)

消费者:

def consume():
while True:
try:
result = redis.brpop("task_queue", timeout=5)
if result is None:
continue
_, task_json = result
process(task_json)
except Exception as e:
logger.error(f"处理失败: {e}")
# 注意:消息已经丢了!

但这段代码藏了一个致命问题——BRPOP 拿到消息后立即从 List 删除了。如果 process(task_json) 抛了异常或者进程崩溃,这条消息就没了。

这就是 List 作为消息队列的核心局限:没有 ACK 机制。消息一旦弹出就没有回头路。

补救:备份队列#

LMOVE(Redis 6.2+)和它的阻塞版本 BLMOVE 可以在弹出消息的同时原子性地备份一份:

# 从 source 弹出,原子性地推入 backup
BLMOVE task_queue backup_queue RIGHT LEFT 5

消费者处理成功后,再从 backup 里删掉。如果消费者崩溃了,可以从 backup 队列里恢复未处理的消息。

但这毕竟是自己搭的轮子,生产环境对消息可靠性有要求的话,Redis 5.0+ 的 Stream 是官方推荐方案——自带 ACK、消费者组、消息回溯。下一篇介绍 Stream 的时候会细聊。

什么时候用 List 做队列,什么时候用 Stream#

你的需求推荐
消息丢了无所谓(比如通知类)List 够用
单消费者、简单场景List 省事
消息不能丢、需要重试Stream
需要多个消费者并行消费同一个队列Stream 消费者组
需要回溯历史消息Stream
已经部署了 Kafka/RabbitMQ用现成的,别用 Redis

List 内部怎么存的:Quicklist#

前面聊 Hash 的时候说了 listpack 省内存。List 也用 listpack,但组织方式不一样。

List 底层是 quicklist——一个双向链表,每个节点里放一个 listpack。

head → [listpack: a, b, c, d] ↔ [listpack: e, f, g] ↔ [listpack: h, i] ← tail

为什么不用一个巨大的 listpack?因为 List 经常在两端操作,如果全放在一个连续内存块里,头部插入要移动所有元素,O(n)。而 quicklist 中部插入只需要动一个节点。

为什么不用纯链表(每个节点一个元素)?每个链表节点有指针开销(prev/next),几十万个元素就是几十万对指针,内存浪费严重。quicklist 每个节点装多个元素,摊薄了指针成本。

中间不常用的节点还会被 LZF 压缩,首尾节点保持未压缩状态以保证两端操作的性能。

两个关键配置:

list-max-listpack-size -2 # 每个节点最大 8KB(-2 是默认)
list-compress-depth 0 # 0=不压缩,1=首尾各 1 个不压缩

一般不用改。但如果你用 List 做消息队列且消息量很大,把 list-compress-depth 设为 1 或 2 能省不少内存。

几个顺手的小技巧#

固定长度的”最近 N 条”#

LTRIM 可以保留 List 的指定范围,砍掉其余部分:

LPUSH recent_logs "log_entry_6"
LTRIM recent_logs 0 4 # 只保留最新的 5 条

配合 RPUSH 使用,天然是一个”最近 N 条记录”的存储结构——比如最近 100 条操作日志、最近 50 次登录记录。

分页查 List#

LRANGE 支持范围查询,天然支持分页:

LRANGE messages 0 19 # 第 1 页(20 条)
LRANGE messages 20 39 # 第 2 页

但注意 LRANGE 是 O(n) 的——n 是你请求的范围大小,跟 List 总长度无关。所以翻页性能不会因为 List 变长而恶化。不过翻太深(比如 LRANGE messages 5000 5019)还是慢,考虑换数据结构。

原子转移:LMOVE#

Redis 6.2 开始 LMOVE 可以原子性地把一个 List 的元素移到另一个:

LMOVE pending processing RIGHT LEFT
# 从 pending 右边弹出,从 processing 左边推进去

任务状态流转(待处理 → 处理中 → 已完成)用这个很干净,中间不会有”弹出成功但推入失败”的半吊子状态。

查长度#

LLEN mylist

O(1) 操作,Redis 在 quicklist 结构里维护了 count 字段,直接读。

总结#

List 最厉害的就两点:阻塞弹出双向操作

阻塞弹出让它从”数据结构”跨到了”基础设施”——一个极简的消息队列,不需要额外组件。但消息可靠性、消费者组这些进阶需求,List 满足不了,要向上用 Stream。

双向操作让它同时胜任队列和栈,加上 LMOVELTRIM 这些周边命令,在日常开发里出现的频率比想象中高得多。

下一篇聊 Set——无序不重复、交集并集差集。共同好友、标签系统、随机抽奖,都藏在 Set 里。


这是 Redis 学习系列的第 4 篇。下一篇:Redis学习系列 | 共同好友原来这么算

Redis学习系列 | 先入先出?还能后进先出?
https://qiandaos.top/posts/redis-learning-series/4-list/
作者
千岛寒流
发布于
2025-06-01
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

封面
Sample Song
Sample Artist
封面
Sample Song
Sample Artist
0:00 / 0:00