go语言无原生延迟队列,生产环境须用redis zset+lua(毫秒时间戳、原子查删推)、rabbitmq延迟插件(毫秒单位、非官方兼容风险)或mysql(强一致、防轮询压库),time.afterfunc仅适用于可丢失的单机短时任务。

Go 语言没有原生延迟队列,time.AfterFunc 只能用于单机、可丢失、短时任务;生产环境必须依赖外部存储或中间件,否则进程重启即丢消息、无法水平扩展、无幂等保障。
Redis ZSET + Lua 是中小项目最稳的起点
核心是用 ZADD 存毫秒级时间戳为 score,消息体为 member,再用 Lua 脚本原子性完成「查到期消息 → 删除 → 推入待消费 LIST」三步。不封装进 Lua,多 worker 并发扫同一时间窗口必然重复投递。
-
score必须用time.Now().UnixMilli(),别用Unix():秒级精度在高并发下易一次拉出数百条,OOM 风险高 - Lua 脚本里
KEYS[1]是 ZSET key(如"delay_zset"),KEYS[2]是目标 LIST key(如"ready_list"),ARGV[1]是当前毫秒时间戳字符串 - 轮询间隔设 500ms~1s,别用 100ms:先
ZCOUNT delay_zset -inf ARGV[1]判断有无到期项,再扫,省 Redis 连接和 CPU - 消息体必须带唯一
id字段,消费端做幂等——Lua 原子性不是银弹,网络分区或脚本执行中断时仍可能漏删
RabbitMQ 插件方案看着原生,实际要盯死兼容性和单位
启用 rabbitmq_delayed_message_exchange 插件后,发消息时加 header["x-delay"] = "5000" 即可,但插件非官方内置,不同 RabbitMQ 版本行为不一致,升级集群时容易失效。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- Exchange 类型必须声明为
"x-delayed-message",且args中指定"x-delayed-type"(如"direct") -
x-delay单位是毫秒,不是秒:写成"5"就真延迟 5ms,别手抖 - 消息不会进死信队列再转发——插件是在路由阶段拦截,未到时间根本不出队,所以不存在 TTL 过期逻辑
- 延迟精度在百毫秒级,不适合亚秒级调度(如风控超时判定)
MySQL 方案适合强一致但怕轮询压垮 DB
在业务表加 delay_at DATETIME 和 status TINYINT 字段,用 SELECT ... FOR UPDATE SKIP LOCKED 避免并发抢同一行,适合订单关单这类不能丢、需审计的场景。
- WHERE 条件必须加
delay_at ,且建议再加 <code>delay_at > NOW() - INTERVAL 1 MINUTE过滤陈旧扫描 - 更新前先
UPDATE ... SET status = 1 WHERE id = ? AND status = 0做乐观锁,防重复消费 - 高频轮询(如每秒)会显著抬高数据库负载,建议搭配
pg_notify或 Redis Pub/Sub 做轻量唤醒 - 别把延迟逻辑全塞进 SQL:复杂条件判断、JSON 解析等放 Go 层,避免慢查询拖垮主库
真正难的不是怎么存消息,而是怎么让“已取未处理”的消息在崩溃后不丢、不重复、不错序——这要求每一步都带确认机制(如 LIST + ACK + DEL)、每条消息带唯一 ID、每个消费者做幂等,而这些细节在 time.AfterFunc 或裸 ZSET 轮询里全得自己补。










