Redis Lua脚本中无法直接sleep,延迟队列需用ZSET+时间戳模拟:入队ZADD设score为毫秒时间戳,出队用ZRANGEBYSCORE拉取再ZREM移除;Redis 7.0+可用ZPOPMIN LT原子弹出,但仅支持单个。

Redis Lua 脚本里不能直接 sleep 或等待
Redis 是单线程执行 Lua 脚本的,redis.call() 之外的任何阻塞(比如 os.sleep、redis.pcall("sleep", 1))都会报错或被禁用。所谓“延迟出队”,实际是靠时间戳 + 有序集合(ZSET)或带过期时间的键来模拟,不是真等。
用 ZSET 实现延迟队列的出队逻辑
典型做法:把任务放进 ZSET,score 设为 UNIX 时间戳(毫秒级),消费端定期用 ZRANGEBYSCORE 拉取已到时间的任务,再用 ZREM 原子性移除。
- 入队:
ZADD delay_queue 1717023600000 "task:123" - 出队(Lua 脚本内):
ZRANGEBYSCORE delay_queue -inf now LIMIT 0 1,然后ZREM对应成员 - 注意
now必须传入脚本,不能用redis.call("time")再转换,因为TIME返回的是秒+微秒数组,得手动拼成毫秒时间戳 - 如果多个客户端并发拉取,需配合
EVALSHA+WATCH或用lua脚本内ZPOPMIN(Redis 7.0+)避免重复消费
Redis 7.0+ 可用 ZPOPMIN + LT 参数简化逻辑
ZPOPMIN 支持 LT(less than)参数,能原子性弹出 score 小于等于指定值的成员,比手写 ZRANGEBYSCORE + ZREM 更安全。
- 示例脚本:
redis.call("ZPOPMIN", KEYS[1], ARGV[1]),其中ARGV[1]是当前毫秒时间戳 - 但注意:
ZPOPMIN count不支持LT,只能一次弹一个;要批量,仍得回退到ZRANGEBYSCORE + ZREM组合 - 低版本 Redis(ZPOPMIN,强行调用会报错
ERR unknown command 'ZPOPMIN'
用 EXPIRE 配合 SET 实现轻量延迟任务时的坑
有人用 SET key value EX 5 + 监听 key 过期事件(__keyevent@0__:expired)来触发出队,但这不适用于队列场景:
- 过期事件不可靠:Redis 通知可能丢失,尤其在主从切换或网络抖动时
- 无法保证顺序:多个 key 同时过期,事件到达顺序不确定
- 不能回滚:一旦 key 过期被消费,没做幂等就容易丢任务
- 真正适合的场景只有单次定时触发(如发验证码后 5 分钟清理),不适合需要可靠出队的延迟队列
复杂点在于:延迟精度、并发安全、失败重试、任务幂等,这些都得在 Lua 外围服务层补足,脚本只负责原子读-删。别指望一个 EVAL 包打天下。










