Redis用ZSET实现延时队列需设毫秒级时间戳为score,但仅ZADD不够:存在并发重复消费、失败重试混乱、精度不足、内存堆积等风险,须结合Lua原子脚本、动态轮询、指数退避重试及定期清理等机制保障可靠性。

为什么不用 zadd 直接塞时间戳当 score 就完事?
能,但容易翻车。Redis 的 ZSET 确实适合做延时队列——把执行时间戳设为 score,用 zrangebyscore 拉出已到期任务。但问题在「精度」和「并发消费」:zrangebyscore 返回的是快照,如果多个 worker 同时拉取同一段区间,会重复执行;不加锁又没法原子性地“取出并删除”。更糟的是,如果任务执行失败,你得把它放回去,但原 score 可能已过期,重设时得小心别覆盖了其他逻辑。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 用
zremrangebyscore+zrange组合实现“查删分离”:先用zremrangebyscore原子性地摘走一批到期任务(返回被删元素),再用zrange查看它们内容(需提前把任务序列化进 value) - 或改用 Lua 脚本封装整个“取、删、返回”流程,避免中间状态暴露
- score 必须用毫秒时间戳(
time.Now().UnixMilli()),别用秒级——否则同秒内多个任务会因 score 冲突导致排序不可控
怎么让 Go worker 安全轮询且不空转?
轮询不能写成 for { zrange... time.Sleep(100 * time.Millisecond) }——这既浪费连接又压 Redis。核心是用 zrangebyscore 查时带上 WITHSCORES,并始终检查返回结果是否为空;为空就休眠,但休眠时间要动态:刚没拿到任务就睡 100ms,连续 5 次没拿到就升到 500ms,有任务后再回落。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 用
redis.Client.ZRangeByScoreWithScores方法,传入&redis.ZRangeBy{Min: "-inf", Max: fmt.Sprintf("%d", time.Now().UnixMilli())} - 每次轮询前加
context.WithTimeout(ctx, 3*time.Second),防止单次命令卡死 - 不要在 for 循环里反复
defer conn.Close()——连接应复用,用redis.NewClient()初始化一次即可 - 首次启动时可预热:用
ZCARD看队列长度,若 > 0 则立刻触发一轮拉取,避免首屏延迟
任务执行失败后如何可靠重试?
直接 zadd 回原队列风险高:如果失败原因是下游服务雪崩,重插会加剧压力;如果失败是参数错误,重插只是无限循环报错。必须区分失败类型,并控制重试节奏。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 对临时性错误(如网络超时、HTTP 503),用指数退避重插:第 1 次失败后
score = now + 1000(1 秒后),第 2 次+ 2000,第 3 次+ 4000……上限设为 5 分钟 - 对永久性错误(如 JSON 解析失败、必填字段为空),记录到独立的
failed_tasks:Hash 表,带错误堆栈和原始 payload,人工介入 - 所有重插操作必须用 Lua 脚本保证:先
HGET查重试次数,再ZADD,再HINCRBY,三步不可拆 - 任务结构体里显式加
RetryCount int `json:"retry_count"`字段,别靠外部计数器——序列化进 Redis 的数据必须自包含
怎么防止任务堆积导致内存爆炸?
Redis 内存不是无限的,而延时队列天然会积压未到期任务。如果业务方疯狂投递 7 天后执行的任务,又没监控,几小时就能打爆实例。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 投递端强制校验:
if delay > 7*24*time.Hour { return errors.New("max delay is 7 days") } - 定期用
ZCOUNT key (now + 3600) +inf统计远期任务量,告警阈值设为 10 万条 - 后台启一个 goroutine,每 5 分钟执行
ZREMRANGEBYSCORE key -inf (now - 300000)清理已过期 5 分钟仍无人处理的任务(说明已丢失) - 所有任务 value 必须用紧凑格式:优先选
gob或msgpack,别用 JSON——同样结构体,JSON 多 30%~50% 体积
真正麻烦的不是实现,是边界条件:比如系统时间回拨会让大量任务瞬间“到期”,或者 Redis 主从切换期间 ZSET 命令部分丢失。这些没法靠代码完全兜住,得靠运维侧的时钟同步策略和哨兵配置兜底。


















