Redis ZSet + Lua 脚本是中小型项目最稳的延迟队列起点,需用Lua原子执行查、删、推三步,配合唯一ID幂等及合理轮询间隔。

Redis ZSet + Lua 脚本是中小型项目最稳的起点
直接用 redis.ZAdd 存时间戳 + 消息体,再用 ZRangeByScore 扫描到期项,是 Go 里最快落地延迟队列的方式。它不依赖额外中间件,也不用自己维护调度逻辑。
常见错误现象:ZRangeByScore 查出来后直接 ZRem,结果多个 worker 同时扫到同一批消息,导致重复消费;或者只查不删,消息一直卡在 ZSet 里不动。
- 必须用 Lua 脚本原子性完成「查 + 删除 + 推入待执行队列」三步,否则并发下必出问题
-
Min设为"0"、Max动态传time.Now().Unix(),别硬写死时间戳 - 轮询间隔别设太短(比如 100ms),建议 500ms~1s;可先用
ZCount预判是否有活再扫,省连接和 CPU - 消息体必须带唯一
id字段,消费端做幂等判断——Lua 没兜住时还有最后一道防线
time.AfterFunc 只适合单机轻量定时,且不能重启存活
time.AfterFunc 本质是往 runtime timer 堆注册回调,轻量、无依赖,但有硬伤:进程挂了就全丢,且无法取消已注册但未触发的任务(它返回值是 void)。
如果你非要临时用,务必满足三个条件:
- 任务失败可接受(比如发个非核心埋点)
- 延迟时间短(
- 调用前做防御判断:
if delay ,否则time.AfterFunc会立刻触发,容易引发意料外执行
别用 time.Sleep 模拟延迟——它阻塞 goroutine,没法取消,也不是队列。
RabbitMQ 插件方案看着原生,其实兼容性和精度都得盯紧
如果你已经在用 RabbitMQ,装上 rabbitmq_delayed_message_exchange 插件,几行代码就能让延迟消息走原生路径,不用改消费端逻辑。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
容易踩的坑:
- 插件版本和 RabbitMQ 主版本强绑定,升级前务必查官方兼容表,别直接
apt upgrade - Exchange 类型必须声明为
"x-delayed-message",且args中指定"x-delayed-type"(如"direct") - 延迟值单位是毫秒,不是秒——
header["x-delay"] = "5000"表示 5 秒,写成"5"就真延迟 5ms - 延迟精度在百毫秒级,不适合亚秒级调度;插件是路由时拦截,没到时间根本不投递,不存在 TTL 过期逻辑
MySQL 实现延迟队列更稳妥,但得防数据库被轮询打垮
加 delay_at DATETIME 和 status TINYINT 字段,用 SELECT ... FOR UPDATE SKIP LOCKED 避免并发抢同一行,适合对一致性要求高、吞吐不极端的场景。
性能影响明显:
- 高频轮询会拖慢 DB,建议加过滤条件:
WHERE delay_at NOW() - INTERVAL 1 MINUTE - 索引必须覆盖
delay_at和status,否则FOR UPDATE会锁全表 - 每轮最多取 100 条,处理完批量更新
status,避免单条事务太长
真正复杂的是恢复逻辑:服务重启后,得从 DB 重载所有 status = 0 且 delay_at 的任务,重新注入调度流程——这点最容易漏。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










