不能直接用ZADD时间戳当score,因秒级时间戳高并发下易重复,导致ZSET内按member字典序排序、执行顺序不可控,且同member时ZADD会意外覆盖原任务;应改用毫秒/纳秒时间戳或“毫秒+序列号”确保score全局唯一单调递增。

为什么不能直接用 ZADD 时间戳当 score?
很多人一上来就把任务执行时间戳(比如 time.Now().Unix())作为 ZADD 的 score,然后轮询 ZRANGEBYSCORE 0 (now 拿任务。问题在于:Redis 的 score 是 float64,秒级时间戳在高并发下极易重复;一旦 score 相同,Sorted Set 内部按 member 字典序排序,导致任务执行顺序不可控,甚至被后续同分值插入覆盖(member 相同时 ZADD 会更新 score,但你并不想“更新”一个待执行任务)。
实操建议:
- score 必须全局唯一且单调递增,推荐组合:
unix millisecond + sequence number(如1717023456789.001),或更简单——直接用纳秒时间戳(time.Now().UnixNano()),它在单机上基本不重复,且天然有序 - 避免用秒/毫秒级时间戳 + 随机数拼接,随机数可能倒序,破坏单调性
- member 推荐用唯一任务 ID(如 UUID 或业务主键),不要放原始 payload,否则影响
ZCARD统计和范围查询效率
如何安全地取出并标记“正在执行”的任务?
单纯 ZRANGEBYSCORE + ZREM 是竞态的:两个 worker 同时查到同一任务,都取走执行,造成重复。必须原子化“取出 + 标记”。Redis 没有内置的“弹出最小 score 并返回”,但可用 Lua 脚本实现:
local tasks = redis.call('ZRANGEBYSCORE', KEYS[1], '-inf', ARGV[1], 'LIMIT', 0, tonumber(ARGV[2]))
if #tasks == 0 then
return {}
end
-- 原子性移除这些 task
redis.call('ZREM', KEYS[1], unpack(tasks))
return tasks
Go 中调用示例:
script.Load(client).Run(ctx, []string{queueKey}, nowUnixMilli, batchSize) —— 注意:传入的 nowUnixMilli 是当前时间毫秒,batchSize 控制每次最多取多少个,防止单次处理太久阻塞其他 worker
常见错误:
- 用
ZPOPMIN?它只支持 Redis 6.2+,且 pop 后无法回滚;延迟任务常需失败重试,pop 即丢失,不适合 - 先
ZRANGEBYSCORE再ZREM分两步:网络分区或进程崩溃时,任务可能被查到但未删,下次又被取到,重复执行 - 脚本里没加
LIMIT:大延迟队列中低分值任务堆积,一次扫太多,内存暴涨或超时
任务执行失败后怎么放回队列?
失败不等于丢弃。应把任务以稍后的时间重新写入 Sorted Set,比如延后 30 秒再试:
client.ZAdd(ctx, queueKey, &redis.Z{Score: float64(time.Now().Add(30 * time.Second).UnixMilli()), Member: taskID})
关键点:
- 不要用原 score + 固定偏移(如
oldScore + 30000),因为 oldScore 可能是纳秒级,加毫秒会失真;务必重新计算目标时间戳 - 建议限制最大重试次数,可在任务 payload 里存
retry_count字段,每次重入前检查,超限则投递到死信队列(另一个 Redis key 或 Kafka) - 避免“立即重试”(score 设为 now):容易打满 worker,形成雪崩;至少设 100ms 以上退避
如何避免 worker 无限扫描空队列?
轮询逻辑如果写成 for { scan; time.Sleep(100 * time.Millisecond) },在无任务时纯属空转耗 CPU 和 Redis QPS。正确做法是结合 BLPOP 思路,但 Sorted Set 无原生阻塞命令,得变通:
- 首次扫描为空时,用
redis.Client.BZPOPMIN监听一个辅助的 list(如delay_queue_notify),同时起一个 goroutine 在每次成功ZADD后LPUSH delay_queue_notify "1"做通知 - 更轻量的做法:空扫后
time.Sleep指数退避(100ms → 200ms → 400ms…),上限 5s,有任务则重置为 100ms - 千万别用
KEYS或SCAN查 Sorted Set 长度代替ZCARD:前者 O(N),后者 O(1),高频调用下差距巨大
最易被忽略的是时间精度对齐:Go 的 time.Now().UnixMilli() 和 Redis 的 TIME 可能有几十毫秒偏差,尤其跨机房部署时。生产环境务必用 NTP 同步所有节点时钟,或统一用服务端时间(如从 Redis 执行 TIME 命令取时间戳)生成 score,否则任务可能“提前”或“永久滞留”。


















