Redis BLMOVE 怎么实现可恢复的阻塞队列
来源:17golang原创
时间:2026-09-28 07:15:41 460浏览 收藏
Redis 的 BLMOVE 可以把“等待任务”和“处理中任务”分开:工作进程领取任务时,不是把元素直接弹出后交给客户端,而是在 Redis 内把任务 ID 从 pending 原子移动到 processing。即使工作进程随后崩溃,任务仍留在备份列表里,回收器可以把超时任务重新放回待处理队列。
BLMOVE pending processing RIGHT LEFT 5会等待任务,并在取到元素时一次完成弹出与压入。- 处理成功后再用
LREM processing 1 job_id确认;任务值应使用全局唯一 ID。 - 真正可恢复还需要
claimed_at_ms、claim_token、超时回收和业务幂等,语义通常是至少一次投递。
为什么普通阻塞弹出会留下丢任务窗口
BLPOP 或 BRPOP 返回元素时,元素已经从列表删除。若工作进程在“收到任务”和“处理完成”之间崩溃,Redis 中没有第二份在途记录,任务就可能永久消失。这个风险不在阻塞本身,而在领取动作把任务从服务器状态中彻底移除了。
可恢复队列要保护的核心资产,是“已经领取但尚未确认”的任务。最小数据模型通常包含两个 List:queue:pending 保存未领取任务,queue:processing 保存正在处理的任务。队列里只放唯一任务 ID,完整载荷、领取时间、尝试次数和令牌放在对应 Hash 中,便于独立更新状态。
# 生产者写入唯一任务 ID;详细载荷保存在对应 Hash 中。
redis-cli HSET queue:job:job-9f2 payload '{"kind":"email"}' status pending attempts 0
redis-cli LPUSH queue:pending job-9f2
# 查看两个列表的长度,不能把这一步当成处理成功证明。
redis-cli LLEN queue:pending
redis-cli LLEN queue:processing
用 BLMOVE 把领取动作变成原子移动
BLMOVE source destination wherefrom whereto timeout 从 Redis 6.2 起可用。它在源列表为空时阻塞;一旦有元素,就在同一个命令中完成从源端弹出、向目标端压入并把元素返回给客户端。这个移动是原子的,所以不会出现“已经从 pending 删除,但还没写入 processing”这一段中间窗口。
采用 LPUSH 入队时,可以用 RIGHT LEFT 从 pending 右端领取并压到 processing 左端,保持先进先出的消费方向。超时参数为 5 表示最多等待 5 秒;到时没有任务会返回空值,工作进程应继续下一轮,而不是把它记成任务失败。设为 0 会无限阻塞,若进程还要响应关闭信号,使用有限超时通常更容易控制。
# 从 pending 右端领取,并原子压入 processing 左端;最多阻塞 5 秒。
redis-cli BLMOVE queue:pending queue:processing RIGHT LEFT 5
# 该检查只用于观察在途任务,不修改队列状态。
redis-cli LRANGE queue:processing 0 -1

BLMOVE 是旧式 BRPOPLPUSH 模式的现代替代命令,并允许分别选择源端和目标端。不过,它只保证这次列表移动的原子性;移动以后更新任务 Hash、执行外部业务和删除 processing 元素,仍是后续状态变化,不能误认为整个业务事务已经完成。
处理成功后再确认删除
工作进程拿到任务 ID 后,先读取载荷并执行业务。只有业务结果已经持久化,才能从 processing 删除这个 ID。LREM key count element 的 count=1 会从表头方向删除第一个匹配项,因此任务 ID 必须唯一;若多个任务使用相同 JSON 载荷作为列表元素,确认其中一个可能误删另一个。
# 业务完成后只确认当前唯一任务 ID,返回值应为 1。
redis-cli LREM queue:processing 1 job-9f2
# 再更新元数据;生产实现宜用 Lua 将确认与状态更新绑定为一个原子操作。
redis-cli HSET queue:job:job-9f2 status completed completed_at_ms 1790551000000
生产环境更稳妥的做法,是用 Lua 脚本同时校验当前领取令牌、从 processing 删除任务并更新 Hash。这样可以避免 LREM 成功后进程又在更新状态前崩溃,也能让“删除数量不是 1”成为明确的并发冲突信号。不要先删 processing 再开始调用支付、发邮件等外部系统,否则又会重新制造丢任务窗口。
超时回收必须防住迟到确认
只保留 processing 列表还不够,因为 Redis 不知道某个任务是正在正常运行还是工作进程已经失联。领取后应记录 claimed_at_ms、递增 attempts,并生成每次领取都不同的 claim_token。回收器定期查找超过可见性超时的任务,把它从 processing 迁回 pending,并清除或替换旧令牌。
领取令牌解决的是“迟到工作进程”问题:任务 A 超时后被回收并由工作进程 B 重新领取,此时原工作进程 A 可能突然恢复。如果 A 仍能确认任务,就会删除 B 正在处理的记录。确认脚本只有在 Hash 中的 claim_token 与调用方持有的令牌一致时才允许完成,旧工作进程因此无法越过所有权边界。

-- KEYS[1] 是 processing,KEYS[2] 是任务 Hash;ARGV[1] 是任务 ID,ARGV[2] 是领取令牌。
if redis.call('HGET', KEYS[2], 'claim_token') ~= ARGV[2] then
return 0 -- 令牌不匹配,拒绝迟到工作进程确认。
end
local removed = redis.call('LREM', KEYS[1], 1, ARGV[1])
if removed == 1 then
redis.call('HSET', KEYS[2], 'status', 'completed') -- 只有在途记录存在时才完成。
end
return removed
回收同样应做成原子状态变化:先核对状态、时间和令牌,再移动列表并更新 Hash。若任务执行时间本来就可能超过可见性超时,需要心跳续期或按任务类型配置不同超时,不能简单把所有长任务都当作失联。
如何判断这套队列真的可恢复
这类 List 队列通常提供至少一次投递,而不是恰好一次。任务可能在业务动作完成后、确认脚本执行前崩溃,于是回收后再次执行。消费者必须用业务幂等键防止重复扣款、重复发券或重复写入;如果业务无法幂等,就要把结果状态放到能参与条件写入的持久层中。
| 检查项 | 应该观察什么 | 异常意味着什么 |
|---|---|---|
| pending 长度 | 生产高峰后能逐步下降 | 消费能力不足或工作进程停止 |
| processing 长度 | 与并发量和处理时长相符 | 确认失败或任务长期卡住 |
| 超时任务数 | 偶发且可被回收 | 超时配置过短或下游不稳定 |
| 令牌冲突数 | 只在回收与迟到确认竞争时出现 | 工作进程频繁停顿或重复确认 |
| 幂等命中数 | 重试时阻止重复副作用 | 至少一次投递正在触发业务去重 |
恢复演练至少覆盖三种中断点:领取后立即退出、业务完成但确认前退出、任务超时后旧工作进程恢复。检查任务最终是否仍在 pending、processing 或 completed 三者之一,且同一任务 ID 不应同时存在于多个活动列表。若需要消费组、待确认历史、自动认领和更丰富的消息语义,可以进一步评估 Redis Streams;简单 List 队列则胜在模型直观、命令少。
相关问题
BLMOVE 的 timeout 设为 0 会怎样?
工作进程会一直阻塞到源列表出现元素。若程序需要定期检查关闭信号或执行维护任务,使用有限超时并循环领取通常更容易优雅退出。
为什么 processing 里最好只放任务 ID?
唯一 ID 让 LREM count=1 的确认目标明确,也方便把载荷、尝试次数、领取时间和令牌放在 Hash 中独立维护。直接放重复 JSON 会增加误删和状态同步难度。
BLMOVE 能保证任务只执行一次吗?
不能。它保证的是列表间移动原子性。工作进程可能在业务完成后、确认前崩溃,任务会被回收并重试,因此仍要设计幂等处理。
什么时候应该改用 Redis Streams?
当系统需要消费组、待确认条目、多个消费者协调、消息历史和更系统的认领机制时,Streams 更合适;若只是单队列加简单重试,List 加 BLMOVE 的维护成本更低。
-
471 收藏
-
413 收藏
-
327 收藏
-
169 收藏
-
244 收藏
-
177 收藏
-
313 收藏
-
411 收藏
-
193 收藏
-
222 收藏
-
109 收藏
-
369 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习