List 阻塞弹出与队列裁剪

举报
yd_287908631 发表于 2026/11/06 00:33:11 2026/11/06
【摘要】 剖析 List 阻塞弹出、LMOVE 安全队列与 LTRIM 定长列表的落地写法

List 是最早被当队列使用的 Redis 类型,Redis 7 中底层是 quicklist 套 listpack 节点。阻塞弹出、LMOVE、LTRIM 三个能力组合起来,可以实现安全队列和定长 feed 流,但每个命令都有不显眼的边界行为。本文以 Redis 7 为基准逐个拆解。

一、命令速览与复杂度

LPUSH/RPUSH 从头尾插入,返回插入后的列表长度;LPOP/RPOP 弹出头尾元素,Redis 6.2 起支持 count 参数一次弹多个;LRANGE 按下标区间取值,负索引 -1 表示最后一个元素;LREM 按值删除,count 为正表示从头到尾删 n 个、为负从尾向头、为 0 删全部;LPOS(Redis 6.2+)返回元素首次出现的下标,配合 RANK 可指定第几次出现。

两个隐含语义容易忽视:LPUSHX/RPUSHX 只在 key 已存在时插入,配合"key 不存在即队列未初始化"的判断,可以防止消费者误建队列;列表弹出最后一个元素后 key 自动删除,空队列不会残留一堆空 key,BLPOP 的阻塞语义也建立在这套生命周期之上。LINSERT 能在指定元素前后插入,但它要先定位再插,是 O(N) 操作,不要用于长列表。

127.0.0.1:6379> RPUSH mq a b c
(integer) 3
127.0.0.1:6379> LRANGE mq 0 -1
1) "a"
2) "b"
3) "c"
127.0.0.1:6379> LPOP mq 2
1) "a"
2) "b"

复杂度上,LPUSH/RPUSH/LPOP/RPOP 是 O(1),而 LINDEX、LSET、LREM 都是 O(N),因为定位要从头或尾逐节点走。List 只适合两端操作的场景,按下标随机读改应该换 Hash。

二、阻塞弹出:BLPOP/BRPOP 的行为细节

BLPOP/BRPOP 在 key 不存在或列表为空时阻塞等待,列表非空则立即返回,行为与 LPOP 一致。三条规则决定了线上表现:

  1. 唤醒顺序:多个客户端同时阻塞在同一个 key 上,新元素到来时按阻塞先后(先进先出)只唤醒一个,天然是公平的消费者分配。
  2. 多 key 优先级:BLPOP k1 k2 5 按 key 的书写顺序检查,返回第一个非空列表的结果,可以利用它做简易优先队列——高优先级队列放前面。
  3. 超时:timeout 单位秒,0 表示一直等,超时返回 nil。Cluster 模式下多个 key 必须落在同一槽,否则报错。BLPOP 支持一次传多个 key,生产者只管往对应优先级的队列 LPUSH,消费者一条命令同时守候多个队列,比轮询简单得多。

还有一条纪律:不要把阻塞命令放进事务或 Lua 里执行。Lua 脚本执行期间 Redis 串行处理、阻塞其他命令,脚本内的阻塞命令会导致脚本卡死,Redis 7 对脚本内调用阻塞命令直接返回错误。需要"有消息就处理、没有就返回"的语义时,在脚本里用 LPOP 判空即可,超时等待交给客户端侧的循环。

127.0.0.1:6379> BLPOP mq 5
1) "mq"
2) "c"

返回值第一项是来源 key,第二项是元素。BRPOPLPUSH 已经被 LMOVE/BLMOVE(Redis 6.2+)取代,新代码不要再使用旧命令。

注意:阻塞的消费者会一直占着一条连接和对应线程,连接池大小必须按阻塞消费者数量预留,否则会出现连接耗尽、业务线程全部卡在 borrow 上的事故。

三、LMOVE 与安全队列模式

直接 LPOP 拿走的消息随即从内存消失,消费者进程一旦崩溃,这条消息就丢了。安全队列的思路是"取出即转移":

# 消费者:从待处理队列左端弹出,立刻放进"进行中"队列右端,30 秒内无消息则超时
BLMOVE queue:task queue:doing LEFT RIGHT 30
# 处理成功后从进行中队列删除
LREM queue:doing 1 <payload>

进程崩溃时消息还留在 queue:doing 里,由巡检任务定期把它搬回 queue:task 重试。这套模式的可靠性完全由业务自己实现:重试次数、死信、幂等都要自己管,Redis 的 List 队列没有消费者组与 ACK 机制——需要这些能力时应该换 Stream(见本系列 17 篇)或专业消息队列,而不是在 List 上硬造。

巡检任务的写法:定时扫描 queue:doing,对每条记录检查业务侧状态——已完成就 LREM 清掉,超时未完成就 LMOVE 搬回 queue:task 并给消息里的重试计数加一,超过上限的转入 queue:dead 人工处理。巡检频率按业务容忍的恢复延迟定,通常 10 秒到 1 分钟一次足够,注意扫描要配合 LLEN 限制单轮数量,避免 doing 队列本身膨胀成大 key。

四、LTRIM 与定长列表

LPUSH 加 LTRIM 的组合可以维护一个只保留最近 N 条的列表:

127.0.0.1:6379> LPUSH feed:1001 "msg-1024"
(integer) 1025
127.0.0.1:6379> LTRIM feed:1001 0 99
OK

LTRIM 只保留区间 [0, 99] 内的元素,其余裁掉。最近动态、操作日志、失败记录这类"只要最新一段"的场景都适用,两条命令用 pipeline 合并保证每次写入后列表都被裁剪。分页展示用 LRANGE 按固定窗口取,例如第 3 页是 LRANGE feed:1001 20 29;页深越大成本越高(O(S+N)),翻页深的产品应该改用游标(记下上一页最后一个下标)或直接限制最大页数。

LTRIM 的区间是闭区间且支持负索引:LTRIM feed:1001 -100 -1 只保留最后 100 条,适合 RPUSH 追加、展示最新记录的写法;正索引写法则配合 LPUSH"新的在前"。两种方向选一种并全团队统一,混用会让下标语义混乱,裁错方向的 LTRIM 会把列表裁得只剩几条,属于很难恢复的操作。

常见坑 / 最佳实践

  1. 阻塞弹出必须匹配连接池与线程池容量。 每个 BLPOP 独占一条连接,消费者线程数、连接池 max-active、Redis maxclients 三者要一起评估,避免互相拖死。
  2. 禁用长列表上的 LRANGE 0 -1。 一次拉全列表既占带宽又阻塞主线程,长度超过几千的列表改分批 LRANGE,配合 LLEN 判断总量。
  3. 定长列表必须 LPUSH 后紧跟 LTRIM。 漏掉 LTRIM 列表会无限增长成大 key;裁剪窗口大小要按展示需求定,不要拍脑袋设 10 万。
  4. LSET/LINDEX 是 O(N),不要把 List 当数组用。 随机读写场景换 Hash 或本地结构,List 只做先进先出和定长窗口。
  5. 纯 LPOP 的队列在消费者崩溃时丢消息。 关键任务链路必须配 BLMOVE 安全队列加巡检任务,并保证消费逻辑幂等。

小结

  • List 的头尾操作是 O(1),LINDEX/LSET/LREM 是 O(N),只把它当队列和定长窗口用,不要当数组。
  • BLPOP/BRPOP 只在空列表上阻塞,多客户端按先来先服务唤醒,多 key 按书写顺序构成优先级。
  • BLMOVE 实现"取出即转移"的安全队列,配巡检任务兜底;需要消费者组与 ACK 时改用 Stream。
  • LPUSH + LTRIM 维护定长列表,分页用固定窗口 LRANGE,翻页深度要有上限。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。