Redis队列处理失败任务可见性超时
来源:17golang原创
时间:2026-11-15 11:32:56 379浏览 收藏
Redis 队列里的任务一旦被消费者取走,原队列就看不见它了。消费者进程崩溃、网络断开或业务处理失败时,如果没有回收机制,这个任务就会“丢失”。实用的做法是把领取动作设计成一次 lease:任务从 pending 原子转入 processing,同时记录到期时间;worker 成功确认时删除它,回收器只把已到期任务重新放回队列。
需要注意,可见性超时不是 Redis 某个命令自动提供的字段。Redis 官方的 LMOVE/BLMOVE 可靠队列模式负责原子转移,超时扫描和重新入队仍由应用补上。
- pending List 只放可领取的任务,processing Sorted Set 用 score 保存 lease 到期时间。
- 每次领取生成 claim token,确认时比对当前 token,防止旧 worker 的晚到 ack 误删新 lease。
- 可见性超时要覆盖正常处理时长并留余量;重试次数超过上限就进 dead-letter。
把任务状态拆成三块
一个任务至少需要 job ID、payload、attempts、status 和 claim_token。可以按下面的方式建模:
| Redis 对象 | 用途 | 关键字段 |
|---|---|---|
queue:jobs:pending | 等待领取的 List | job ID |
queue:jobs:processing | 按到期时间排序的 Sorted Set | score = lease_until,member = job ID |
job:{id} | 任务元数据 Hash | payload、attempts、claim_token |

领取时可以使用 BLMOVE queue:jobs:pending queue:jobs:processing RIGHT LEFT 5,它解决“弹出后进程立刻崩溃”的原子性问题。若要按时间精确回收,生产实现通常让 processing Sorted Set 保存 job ID,而不是只依赖一个普通 processing List。
# 下面的命令只展示数据结构,不代表已经在本机执行
LPUSH queue:jobs:pending job-1001
HSET job:1001 payload "send-email" attempts 1 claim_token "token-A"
ZADD queue:jobs:processing 1794713612000 job-1001
# score 是毫秒级 lease 到期时间,回收器按它扫描到期任务
用 lease_until 维护可见性超时
回收器每次取一小批 ZRANGEBYSCORE queue:jobs:processing -inf now LIMIT 0 100,再检查任务 Hash 的状态和 token。到期不等于立即失败:有些任务只是处理时间长,所以 worker 的续租应该在预计到期前完成,且续租必须校验当前 token。
时间参数可以先按“正常处理 P99 + 网络和调度余量”估算。例如大多数任务 20 秒完成,可见性超时先设 60 秒,再根据重复执行率调整。设置成 5 秒并不能让失败更快恢复,反而容易把仍在运行的任务重复投递。
让确认和回收互相排他
最危险的竞态是:worker A 超时后失联,回收器把任务放回 pending,worker B 重新领取;此时 A 又恢复并发送成功确认。如果确认只执行 DEL job:{id} 或 ZREM,就可能把 B 的新任务状态删掉。
因此,确认、续租和回收都要带当前 claim token,并在同一个 Lua 原子边界里做“读取—比较—修改”。下面是确认逻辑的精简示意:
-- KEYS[1] 是 job Hash,KEYS[2] 是 processing ZSET
-- 只有 token 仍属于当前 worker,才允许确认并移除处理记录
local current = redis.call('HGET', KEYS[1], 'claim_token')
if current ~= ARGV[1] then
return {0, 'stale-token'} -- 旧 worker 的晚到 ack 必须被拒绝
end
redis.call('HSET', KEYS[1], 'status', 'done')
redis.call('ZREM', KEYS[2], ARGV[2])
return {1, 'acked'}

失败重试要有出口
回收不是无限重试。每次重新入队前递增 attempts,低于上限就写入新的 token 和新的 lease;达到上限则把 job ID 放进 queue:jobs:dead,保留最后错误原因,交给人工或补偿任务处理。回收脚本还要做到幂等:同一个 job 已被成功确认或已被另一个 worker 领取时,重复扫描不能再次入队。
- 确认成功:状态改为 done,从 processing 移除,按保留策略清理 payload。
- 业务可重试失败:增加 attempts,生成新 token,重新设置 lease。
- 超过上限:进入 dead-letter,保留错误摘要和最后一次 lease 时间。
常见问题
Redis 的 BLPOP 能直接提供可见性超时吗?
不能。BLPOP 只负责阻塞等待并弹出元素;要避免丢失,应使用 LMOVE/BLMOVE 或等价的原子转移,再由应用维护 processing 和回收。
可见性超时应该设置多长?
以正常处理时长的高分位数为基线,再加网络、调度和续租余量;太短会重复执行,太长会延迟故障恢复。
为什么一定要 claim token?
因为同一个 job ID 可能经历多次领取。token 能区分不同 lease,让旧 worker 的确认变成无效操作。
Sorted Set 能替代任务的完整状态吗?
不建议。Sorted Set 适合索引到期时间,payload、attempts、status 和错误信息仍应放在 Hash 或持久化存储中。
-
311 收藏
-
483 收藏
-
172 收藏
-
464 收藏
-
239 收藏
-
331 收藏
-
421 收藏
-
184 收藏
-
209 收藏
-
406 收藏
-
135 收藏
-
401 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习