不能直接用ZRANGEBYSCORE+ZREM组合,因二者非原子执行存在竞态窗口:多worker同时扫描会重复获取同一消息,导致重复消费或panic丢失;ZPOPMIN不标记状态致崩溃任务无法恢复;必须用Lua脚本原子封装查、删、推三步,并用ZREMRANGEBYSCORE批量删除防性能下跌。

为什么不能直接用 ZRANGEBYSCORE + ZREM 组合
常见错误是轮询时先 ZRANGEBYSCORE 拿一批消息,再逐条 ZREM 删除——这中间存在竞态窗口。两个 worker 同时扫到同一时间窗口,都拿到 "order:123",一个成功消费并删掉,另一个也去删(可能部分失败),但消息体早已被取走,结果就是重复执行,或某条因 panic 没进业务逻辑就从 ZSET 消失,彻底丢失。
Redis 不保证多命令原子性;ZPOPMIN 看似能解决,但它只弹出不标记,worker 崩溃后任务无法恢复;ZREMRANGEBYSCORE 又无法区分“已取未处理”和“真到期”,容易误删。
- 必须用 Lua 脚本把「查 + 删 + 推入待消费 LIST」三步锁死在 Redis 服务端执行
- 脚本里用
ZREMRANGEBYSCORE删除整段,不是逐条ZREM,否则高并发下性能断崖下跌 - 消息体必须带唯一
id字段,消费端做幂等——Lua 原子性不是银弹,网络分区或脚本中断时仍可能漏删
如何写一个安全可用的 Lua 脚本
脚本核心是:用 ZRANGEBYSCORE 扫描带 WITHSCORES 的到期消息 → 提取所有 member → 用 ZREMRANGEBYSCORE 原子删除 → 用 RPUSH 批量推入目标 LIST。
示例脚本 delay_pop.lua:
立即学习“go语言免费学习笔记(深入)”;
local keys = redis.call('ZRANGEBYSCORE', KEYS[1], '-inf', ARGV[1], 'WITHSCORES', 'LIMIT', 0, 100)
if #keys == 0 then return {} end
local members = {}
for i = 1, #keys, 2 do
table.insert(members, keys[i])
end
redis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', ARGV[1])
for _, m in ipairs(members) do
redis.call('RPUSH', KEYS[2], m)
end
return members-
KEYS[1]是 ZSET key(如"delay_zset"),KEYS[2]是目标 LIST key(如"ready_list") -
ARGV[1]必须是当前毫秒时间戳字符串,用strconv.FormatInt(time.Now().UnixMilli(), 10)生成,别硬写死 - 别用
time.Now().UnixNano()——Redis score 是 double 类型,纳秒转 float64 会丢失精度 -
LIMIT 0 100控制单次最多拉取条数,防 OOM 和 Redis 阻塞
Go 中调用 Eval 时的关键细节
go-redis/v9 的 client.Eval 是入口,但参数传错就白忙活。常见问题包括:score 单位不一致、key 名拼错、context 超时没设、连接没复用。
-
ctx必须带超时:ctx, cancel := context.WithTimeout(ctx, 3*time.Second),防止单次卡死阻塞 goroutine -
KEYS参数传的是真实 Redis key 名字符串,不是变量名;ARGV是[]interface{},其中ARGV[0]是时间戳字符串 - 别在 for 循环里反复
defer conn.Close()——连接必须复用,用redis.NewClient()初始化一次 - 轮询前建议先
ZCOUNT delay_zset -inf ARGV[1]判断有无到期项,避免空轮询打满 Redis QPS
轮询节奏与容错怎么配才不翻车
每 100ms 扫一次看似响应快,实则浪费连接、抬高 CPU、打满 Redis QPS;更糟的是空轮询时没限流,Redis 线程一直忙等。生产环境别这么干。
- 基础轮询间隔设为
500ms ~ 1s,首次启动时用ZCARD预热,有积压立刻拉取 - 扫描范围建议加缓冲窗口,比如查
now_ms - 5000到now_ms,防 Redis 与应用服务器时间不同步 - 消息体建议存 JSON,含
id、payload、created_at字段,消费端用id做幂等判断 - 真正难的不是怎么存消息,而是怎么让“已取未处理”的消息在崩溃后不丢、不重复、不错序——这要求每一步都带确认机制(如 LIST + ACK + DEL),而这些细节在裸 ZSET 轮询里全得自己补


















