zpopmin是可靠起点但单靠它不够:需毫秒级score精度、zcount预检防空取、delay_processing状态可恢复(lua原子回滚)、delay_queue设ttl,全链路状态变更必须原子化。

ZPOPMIN 是可靠起点,但单靠它不够
直接用 ZPOPMIN 替代轮询 ZRANGEBYSCORE 能解决竞态和空查问题,但它只管“取”,不管“后续怎么活”。真实线上跑三天就出问题的,基本都卡在这几个环节:
-
ZPOPMIN返回后 consumer panic,任务既没处理也没标记,彻底消失 -
delay_processinghash 里堆满超时未清理的 entry,守护程序反复回滚同一任务 - score 用
time.Now().Unix(),同秒内插入 20 条任务,ZPOPMIN一次吐出全部,业务根本扛不住并发压测
score 精度必须是毫秒,且要预检
用秒级时间戳当 score 是最常见的时间精度翻车点。哪怕业务延迟要求是分钟级,也得用 time.Now().UnixMilli():
- Redis 的
ZPOPMIN按 score 升序返回,score 相同则按 member 字典序——这不是可控排序,是随机乱序 - 一旦单秒内任务量 >10 条,
ZPOPMIN delay_queue 10可能返回一堆本该错峰执行的任务,瞬间打垮下游 - 生产必须加预检:
ZCOUNT delay_queue -inf <now_ms></now_ms>返回 0 就跳过ZPOPMIN,避免无效调用
delay_processing 状态必须可恢复,不能只靠 HDEL
很多人写成“取完立刻 HSET delay_processing key timestamp,处理完 HDEL delay_processing key”,这在 panic、OOM、网络中断时必然留脏数据。正确做法是:
- 守护 goroutine 每 30 秒扫一次
delay_processing,用HGETALL拿全量 - 对每个 entry 检查
timestamp是否超时(阈值设为业务最长耗时 ×2,比如 120 秒) - 发现超时,先
HEXISTS delay_processing key二次确认,再用 Lua 脚本原子执行:ZADD delay_queue <new_score> key</new_score>+HDEL delay_processing key - 切记:回滚动作若拆成两个 Redis 命令,ZADD 成功但 HDEL 失败,就会无限循环重投
TTL 和 Lua 全链路封装是底线
delay_queue 这个 zset key 必须显式设 TTL:EXPIRE delay_queue 86400,否则失败任务、测试残留永久堆积,内存只增不减。Lua 脚本不能只包“取+标”,必须覆盖“取 → 标 → 推送”三步,否则中间任意一环失败,状态就断了。真正难的不是写对第一次,而是让系统在节点宕机、网络分区、consumer crash 后还能自己续上——所有状态变更都得有反向可逆路径,且每条路径都得进 Lua。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











