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

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等待领取的 Listjob ID
queue:jobs:processing按到期时间排序的 Sorted Setscore = lease_until,member = job ID
job:{id}任务元数据 Hashpayload、attempts、claim_token
Redis 队列 pending list、processing Sorted Set 与任务 Hash 的可见性超时结构说明图
图1:Redis 队列状态结构说明图,展示待领取、处理中与任务元数据的边界,不是运行截图。

领取时可以使用 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'}
Redis worker A 与 worker B 经过 lease 过期回收后用 token 拒绝旧确认的关系说明图
图2:可见性超时回收关系说明图,展示 token 校验如何阻断旧消费者的晚到确认,不是运行截图。

失败重试要有出口

回收不是无限重试。每次重新入队前递增 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 或持久化存储中。

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