首页/数据/数据库速查/延时队列与到期扫描(ZSet 场景)
Redis 命令96 条

延时队列与到期扫描(ZSet 场景)

score 存到期时间戳:ZADD 入队、ZRANGEBYSCORE -inf now 扫到期、ZREM/Lua 原子领取;比轮询全部键高效得多。

数据库速查数据更新于 2026-10-13

延时队列怎么实现

常见的做法是把「到期时间戳」当分数存进有序集合:入队就是 ZADD,扫描到期任务就是按分数做范围查询,领取任务则必须保证「判断与删除」是原子的。相比轮询所有键,有序集合天然按分数有序,定位即将到期的元素代价是 O(log N),实现简单且稳定。

基本信息

结构ZSet,成员为任务标识,分数为到期时间戳
入队ZADD,重复成员名会覆盖,任务标识必须唯一
扫描ZRANGEBYSCORE -inf now LIMIT 0 N WITHSCORES
领取Lua 判断并 ZREM,或用 ZPOPMIN
时间粒度建议毫秒时间戳,在 2^53 以内整数精确

典型使用场景

  • 订单超时未支付自动关闭
  • 定时短信、推送与站内提醒
  • 失败任务延迟重试,例如 30 秒后再次投递
  • 活动定时开始、定时结束与状态切换
  • 分布式定时任务的到期扫描与批量触发

入队、扫描与原子领取

入队、扫描与原子领取
ZADD delay:sms 1735689600000 msg:1        # 到期时间戳作为分数
ZRANGEBYSCORE delay:sms -inf $(date +%s%3N) LIMIT 0 20 WITHSCORES
# 原子领取:先判断到期再删除,避免多消费者重复处理
EVAL "local v=redis.call('ZRANGEBYSCORE',KEYS[1],'-inf',ARGV[1],'LIMIT',0,1) if #v==0 then return nil end redis.call('ZREM',KEYS[1],v[1]) return v[1]" 1 delay:sms 1735689600000
ZPOPMIN delay:sms 1        # 简单场景:弹出当前分数最小的成员

要素与做法

要素做法注意事项
分数到期时间戳(毫秒)用秒级时间戳在高并发下会堆积
入队ZADD 成员标识成员名重复会覆盖,需保证唯一
扫描ZRANGEBYSCORE -inf now配合 LIMIT 控制单批处理量
领取ZPOPMIN 或 Lua 判断加 ZREM两条命令分开会重复消费
失败重试重新 ZADD 到未来时间点要限制重试次数,否则死循环
消费者按秒级间隔轮询即可满足多数场景;想进一步降低延迟可以用阻塞式弹出命令配合定时调度,但要自己处理「还没到期」的情况。
「先查再删」两条独立命令在多个消费者并发时会重复消费同一个任务,必须用 Lua 或 ZPOPMIN 把判断与删除合成一步。
队列积压要能看见:按分数区间统计各时间段的待办数量,就能判断是消费者不足、任务执行超时,还是生产者写入异常增多。

易错点

  • 用秒级时间戳当分数且并发很高:同一秒的任务挤在一起,调度精度不足
  • 扫描与删除拆成两条命令:多消费者并发领取会重复执行同一任务
  • 任务成员名不唯一:ZADD 覆盖后原任务被静默丢弃
  • 没有失败重试与死信处理:异常任务长期积压,无人发现

数据来源与参考

  • Redis 官方文档: ZRANGEBYSCORE
  • Redis 官方文档: ZSet 使用模式

相关工具

​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌