必须用毫秒级时间戳,因秒级会导致同秒内多个任务score相同,zset对相同score的member排序不确定(字典序fallback),引发执行错乱或zrangebyscore漏任务;go中应使用time.now().unixmilli()而非unix()。

为什么 score 必须用毫秒级时间戳,不能用秒
秒级时间戳会导致同一秒内多个任务的 score 完全相同,ZSET 内部对相同 score 的 member 排序是不确定的(按字典序 fallback),可能造成任务执行顺序错乱,甚至 zrangebyscore 拉取时漏掉部分任务。Go 中必须用 time.Now().UnixMilli(),别用 Unix()。
ZRANGEBYSCORE + ZREM 组合为什么不可靠
单纯先 zrangebyscore key -inf now 再 zrem key member1 member2... 是非原子操作:多个 worker 同时执行会拿到同一组任务,各自 zrem 争抢,只有第一个成功,其余 worker 仍会处理已过期任务——这就是重复消费。
- 用
zremrangebyscore key -inf now原子性摘走任务,但返回值只含 member 列表,不带原始 value,你得提前把任务数据序列化进 member 字符串里(比如 JSON + ID 拼接) - 更稳妥的是写 Lua 脚本:一次完成「查出带 score 的任务列表 + 原子删除 + 返回完整数据」,避免中间状态暴露
- 如果任务执行失败,重入队列时不能直接复用原
score,要检查是否已过期;建议新设score = time.Now().UnixMilli() + backoff_duration,并用指数退避(如 1s → 2s → 4s)
worker 轮询不能 sleep 固定间隔
写成 for { zrangebyscore ...; time.Sleep(100 * time.Millisecond) } 会持续占用连接、增加 Redis QPS,且空转浪费资源。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 每次调用
zrangebyscore key -inf now WITHSCORES,检查返回结果长度 - 没任务时休眠时间动态调整:初始 100ms,连续 5 次为空则升到 500ms,一旦取到任务立刻回落到 100ms
- 别用
BLPOP或BRPOP,ZSET 本身不支持阻塞读,强行包装会引入额外 list 中转,增加复杂度和失败点
内存堆积问题怎么定期清理
长期运行后,ZSET 可能积压大量已执行或失败重试超限的任务,导致内存上涨、zrangebyscore 响应变慢。
- 加个后台 goroutine,每 5 分钟执行一次
zremrangebyscore key -inf (now - 86400000)(删掉 24 小时前的所有任务) - 生产环境务必给 ZSET 设置
EXPIRE,比如EXPIRE delay_queue 604800(7 天),防止单点故障导致队列永久滞留 - 别依赖“任务执行完就自动消失”——失败、崩溃、网络中断都会让任务卡住,必须有兜底清理逻辑
实际最易被忽略的是:任务 value 序列化时没包含唯一 traceID 或重试次数字段,导致失败重入后无法判断是否已处理过;还有就是没对 Lua 脚本做 EVALSHA 缓存,高频调用下反而拖慢 Redis。










