您的位置:首页 >Redis ZSet实现延时任务队列方法
发布于2026-04-12 阅读(0)
扫一扫,手机访问
直接用毫秒时间戳作ZSet的score会导致浮点精度丢失和并发重复投递;应将时间戳左移10位并添加自增序列号,兼顾精度、顺序与安全性。

Redis 的 ZSET 确实天然适合做延时队列:把执行时间戳作为 score,任务内容作为 member,用 ZRANGEBYSCORE 就能查出所有到期任务。但直接这么用容易翻车——score 是 double 类型,精度只有 52 位有效位,当时间戳用毫秒(如 1717023456789)时,超过约 2^52 ≈ 4.5e15(即时间戳大于 2255-06-01)后,相邻整数无法被精确表示,导致 ZRANGEBYSCORE 0 1717023456789 漏掉部分任务。
更现实的问题是并发消费:多个 worker 同时 ZRANGEBYSCORE + ZREM,没有原子性,必然重复投递。
必须用 EVAL 或 lua 脚本封装“查+删”逻辑,且 score 设计要预留精度余量。
核心是不直接存毫秒时间戳。推荐两种方案:
1717023456.789123 → 1717023456789123,再转为 string 再转 double 存入 —— 但 Redis 不支持 string score,这条路走不通2^52 安全范围内(当前时间戳 × 1024 < 2^52 直到公元 2100+)示例:任务计划在 1717023456789 毫秒执行,当前已插入 3 个同毫秒任务,则 score = 1717023456789 << 10 | 3 = 1758221999749123。这样既保序,又规避了 double 对大整数的截断。
这是最关键的一步。不能分两步:ZRANGEBYSCORE 后再 ZREM,中间可能被其他 client 修改。
以下脚本从 delayed:queue 中取出最多 n 个 score ≤ now 的任务,并返回它们:
eval "local ms = tonumber(ARGV[1]) local n = tonumber(ARGV[2]) local tasks = redis.call('ZRANGEBYSCORE', KEYS[1], '-inf', ms, 'LIMIT', 0, n) if #tasks > 0 then redis.call('ZREM', KEYS[1], unpack(tasks)) end return tasks" 1 delayed:queue 1717023456789 10
注意点:
ARGV[1] 必须传入数字类型(不是字符串),否则 tonumber() 返回 nil,比较失败unpack(tasks) 在 Redis 7.0+ 已废弃,若用新版需改用 table.unpack(tasks)ZRANGEBYSCORE 返回的是原始 member 字符串,无需额外 decode延时队列必须支持失败回滚。常见错误是:任务取出后执行失败,直接丢弃或仅 log,导致消息丢失。
正确做法是定义一个重试策略,比如最大重试 3 次,每次延迟翻倍:
"{\"id\":\"abc\",\"retry\":2}")now + (2^retry_count * 1000),然后 ZADD delayed:queue dlq:delayed特别注意:重入队列时 score 必须用新计算的时间戳,且仍要套用前面说的“时间戳 × 1024 + seq”编码方式,否则可能因精度问题错序。
实际部署时,score 编码、Lua 原子操作、失败重入这三环缺一不可。最容易被忽略的是 score 的整数编码——很多人图省事直接塞毫秒时间戳,上线跑几个月没事,某天突然发现定时任务批量漏触发,排查半天才想到 double 精度问题。
下一篇:宙斯浏览器关闭自动刷新方法
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
9