登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  数据库 >  Redis

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
Redis BLMOVE 将任务 ID 从 pending 原子移动到 processing 的静态结构图
图1:BLMOVE 在 Redis 内把任务 ID 从 pending 原子移动到 processing,工作进程崩溃时任务仍留在可检查的列表中。

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 与调用方持有的令牌一致时才允许完成,旧工作进程因此无法越过所有权边界。

Redis 处理中列表、领取时间、领取令牌和超时回收器关系结构图
图2:回收器依据 claimed_at_ms 识别过期领取,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 的维护成本更低。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>