当前位置:首页 > 文章列表 > 数据库 > Redis > Redis LMOVE 怎么做可靠队列:processing list、LREM 与重复消费边界

Redis LMOVE 怎么做可靠队列:processing list、LREM 与重复消费边界

来源:17golang原创 2026-08-17 09:40:13 0浏览 收藏

用 Redis 列表搭轻量任务队列,最容易写出的版本是生产者 LPUSH queue task,消费者 RPOP queue。它在演示环境跑起来完全没问题,但消费者刚拿到任务就崩溃时,任务已经从队列里消失,后续没有任何地方能找回。

更稳妥的做法是把“取出任务”和“移入处理中状态”合并成一次原子搬运操作:用 LMOVE queue processing RIGHT LEFT,业务处理成功后再用 LREM processing 1 task 确认删除。这样就能把进程崩溃导致的丢任务问题,转变成“待恢复的处理中任务”场景,要付出的额外代价就是需要处理重复消费和超时回收逻辑。

要点速览

  • LMOVE 把出队和写入 processing list 合并成一次原子操作,BLMOVE 适合队列为空时阻塞等待的场景。
  • 确认删除的动作必须放在业务副作用执行成功之后,搭配唯一任务 ID 和幂等写入逻辑抵御重复消费。
  • 回收任务前必须先做租约校验或者处理时间判断,不能把还在运行的慢任务直接放回待处理队列。
  • Redis Cluster 场景下两个列表要满足多键命令的同槽约束,生产环境上线前建议先用 CLUSTER KEYSLOT 验证。

先把丢任务现场还原出来

假设待发送的通知任务都放在 notify:ready。消费者取出 task:order-1042 后,需要调用下游服务、记录发送结果,再把任务从列表里删掉。

LPUSH notify:ready task:order-1042
RPOP notify:ready
# 进程在调用下游服务前退出

第二条命令的返回值还没返回业务侧,进程就已经退出的话,重启消费者后,notify:ready 里根本找不到这条任务,业务只能依赖额外的数据库记录或者人工补单兜底。这个模型根本算不上“至少一次投递”,本质是“取出即可能丢”。

Redis LMOVE 将待处理队列原子搬入 processing list,并在消费者崩溃时保留任务线索

LMOVE 为什么能保留处理中任务

LMOVE source destination RIGHT LEFT 会从源列表的指定端弹出一个元素,同时把它推入目标列表的指定端。Redis 官方对这个动作的定义是原子操作,单次执行复杂度为 O(1)。消费者不再直接删除任务,而是先完成“待处理 → 处理中”的状态迁移。

LPUSH notify:ready task:order-1042
LMOVE notify:ready notify:processing RIGHT LEFT
# 返回 task:order-1042
LRANGE notify:processing 0 -1

如果消费者在调用支付通知接口前就意外退出,任务还完整留在 notify:processing。后续的恢复程序可以根据任务载荷里的时间戳、租约字段或者外部存储的状态判断要不要重试。这里要注意,“保留任务”只是 LMOVE 的原子搬运带来的特性,它不会自动帮你判断任务是否过期,也不会主动发起重试。

没有任务时用 BLMOVE

反复轮询 LLEN 再执行 LMOVE 不仅会增加网络往返次数,两个命令之间还天然存在竞态窗口。Redis 6.2 及以上版本可以直接用 BLMOVE notify:ready notify:processing RIGHT LEFT 5,队列为空时最多等待 5 秒,拿到元素后还是会完成同一次原子移动。超时后会返回空值,消费者收到空值应该切回心跳、优雅退出或者重新进入等待逻辑,不要把空值当成任务执行失败处理。

确认删除要晚于业务成功

处理函数的执行顺序必须遵守这个规则:先读取任务、校验任务 ID,再执行可重试的业务动作,最后才从 processing list 里删除对应任务。把 LREM 放在业务调用之前,相当于重新制造了“任务已确认但业务副作用没执行完”的丢任务窗口。

# task:order-1042 是完整任务载荷或可回查的唯一 ID
BLMOVE notify:ready notify:processing RIGHT LEFT 5

# 1. 以 task_id 做幂等判断
# 2. 调用下游通知服务
# 3. 记录成功状态
LREM notify:processing 1 task:order-1042

LREM key count element 按元素值删除列表成员。示例里的 1 表示只删除一个匹配项,避免同一任务因为异常重复入列时,一次就清掉所有副本。实际项目里最好让列表元素带上唯一任务 ID,把业务状态存在数据库或者专门的幂等存储里;列表本身并不适合充当完整的审计账本。

Redis processing list 中的 LREM 确认、超时回收和幂等边界

重复消费不是 LMOVE 的 bug

消费者执行完下游动作、还没来得及调用 LREM 就崩溃,任务还是会留在 processing list 里。恢复程序下次重新投递时,就会出现重复消费。这是“至少一次投递”模型的正常表现,不能靠把确认动作提前的错误方式来规避。

幂等键可以选 task_id,例如 order-1042:notify:paid。下游服务成功返回后先往数据库里写一条带唯一约束的完成记录,重复执行时发现这条记录已经存在,就直接跳过重复的副作用逻辑,再继续清理 processing list。如果下游接口支持自定义幂等请求头,也请把同一个任务 ID 贯穿到所有重试请求里。

回收前先判断租约

一个简易的 processing list 只会存任务内容,不会记录每个元素的独立领取时间。生产环境可以把任务领取记录存在 task:lease:{task_id},记录消费者标识、开始处理时间和租约截止时间;定时回收程序只处理明确超过租约时长的任务。没有这层判断的话,慢查询、网络抖动和正常的长任务都可能被误判为“僵尸任务”,导致同一条任务被并发处理。

# 伪代码:回收者必须先核对租约和业务状态
if lease_expired(task_id) and not business_succeeded(task_id):
    LPUSH notify:ready task_id
    LREM notify:processing 1 task_id

回收用到的两条命令不是原子动作,多个回收进程并发运行时,还需要加分布式锁、唯一回收标记或者 Lua/Functions 方案做保护。本文介绍的 LMOVE 方案只解决任务领取阶段的原子迁移问题,不要把它当成能实现 exactly-once 语义的完整队列组件。

上线前核对四个边界

  1. 空队列:BLMOVE 超时返回空值时,消费者是否只记录等待状态而不是写入空任务?
  2. 业务成功后崩溃:幂等记录是否比重试副作用先生效,LREM 是否配置为只删除一个任务副本?
  3. 慢任务回收:是否提前存好了租约截止时间,回收程序能不能正确区分正在运行的慢任务和僵尸任务?
  4. 集群路由:Redis Cluster 环境里 notify:{mail}:ready 与 notify:{mail}:processing 是否使用了同一个 hash tag?上线前可以用 CLUSTER KEYSLOT 对比两者的槽位是否一致。

如果后续任务量、延迟指标和历史追踪需求进一步提升,Redis Streams 的消息 ID、Pending Entries List 和消费组语义会更适配。用列表实现的队列优势是命令少、延迟低,很容易嵌入已有 Redis 依赖的项目里;它的能力边界也很清晰:处理中列表、租约、幂等和回收逻辑都需要业务侧自行实现和维护。

常见问题

LMOVE 和 RPOPLPUSH 有什么区别?

LMOVE 是更通用的列表两端搬运命令,可以自由指定源端和目标端;它覆盖了 RPOPLPUSH 的所有常见用法,也更容易和 BLMOVE 配对使用。

BLMOVE 超时会删除任务吗?

不会。源列表为空且等待超时只会返回空值,不会在处理列表里生成任何元素。只有成功拿到元素的前提下,才会完成那次原子搬运。

为什么处理成功后还要保留幂等记录?

因为进程有可能在业务副作用执行成功后、执行 LREM 前意外退出。幂等记录能让下一次消费直接跳过重复的副作用逻辑,再安全清理掉 processing list 里的残留任务。

Redis Cluster 能直接搬运两个列表吗?

多键命令要求相关键满足集群的路由规则。把两个列表放在同一个 hash tag 下,并用 CLUSTER KEYSLOT 在部署前校验,不能只凭键名前缀就猜测它们会落在同一个槽位。

把确认和恢复写成可验收的契约

可靠列表队列的核心从来不是多调用了一条 Redis 命令,而是明确划分三种状态:待处理、处理中、业务已确认。LMOVE/BLMOVE 负责第一步的原子迁移,LREM 负责处理完成后的清理,幂等键和租约逻辑专门应对崩溃、重试与回收场景。只要每个状态都留好可观测的记录,轻量队列完全可以在不宣称自己是专业消息中间件的前提下稳定运行。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Python 插件注册遇到前向引用怎么办:用 annotationlib 保留 ForwardRef 并安全解析Python 插件注册遇到前向引用怎么办:用 annotationlib 保留 ForwardRef 并安全解析
上一篇
Python 插件注册遇到前向引用怎么办:用 annotationlib 保留 ForwardRef 并安全解析
Redis 8.6 HOTKEYS 怎么定位热键:采样窗口、CPU/网络指标与集群槽位验收
下一篇
Redis 8.6 HOTKEYS 怎么定位热键:采样窗口、CPU/网络指标与集群槽位验收
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    301次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    357次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    356次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    323次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    142次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码