zset实现延迟队列

文章目录

      • [一、 核心设计思想:什么是 ZSet 延迟队列?](#一、 核心设计思想:什么是 ZSet 延迟队列?)
      • [二、 运行流程:存 与 取](#二、 运行流程:存 与 取)
        • [1. 生产者:存入任务(投递消息)](#1. 生产者:存入任务(投递消息))
        • [2. 消费者:取出任务(轮询消费)](#2. 消费者:取出任务(轮询消费))
      • [三、 深度理解:`ZRANGEBYSCORE` 在这里起什么作用?](#三、 深度理解:ZRANGEBYSCORE 在这里起什么作用?)
      • [四、 生产环境必须注意的"隐患"](#四、 生产环境必须注意的“隐患”)

将利用 Redis 的 Sorted Set(ZSet) 实现延迟队列的核心逻辑与 ZRANGEBYSCORE 命令的作用合并,我们可以用" "和""两个动作来完整还原这个方案。


一、 核心设计思想:什么是 ZSet 延迟队列?

Redis 的 ZSet 是一个有序集合 。它里面的每一条数据都会关联一个分数(Score),Redis 会自动根据分数从小到大给数据排好序。

在延迟队列场景中,我们做了一个巧妙的映射:

  • 数据内容(Member) :代表你要处理的任务(比如:订单 ID order_id_10086)。
  • 分数(Score) :代表这个任务的具体执行时间戳 (即 当前时间戳 + 延迟秒数)。

因为 ZSet 会自动排序,所以最先到期的任务,永远排在队列的最前面


二、 运行流程:存 与 取

1. 生产者:存入任务(投递消息)

当用户下单后,系统需要开启一个"10秒后未支付自动取消订单"的延迟任务。假设当前时间戳是 1700000000

生产者计算出执行时间:1700000000 + 10 = 1700000010

然后使用 ZADD 命令把任务塞进名为 delay_queue 的 ZSet 中:

sql 复制代码
ZADD delay_queue 1700000010 "order_id_10086"
2. 消费者:取出任务(轮询消费)

消费者(后台线程)会像一个定时闹钟一样,每隔 1 秒去 Redis 里巡检一次,看看有没有到期的任务。它使用的核心命令就是 ZRANGEBYSCORE

假设现在时间走到了 1700000015(距离下单过去了 15 秒),消费者发起查询:

sql 复制代码
ZRANGEBYSCORE delay_queue 0 1700000015 LIMIT 0 1

三、 深度理解:ZRANGEBYSCORE 在这里起什么作用?

这行命令的字面意思是:"去 delay_queue 里,把分数在 01700000015 之间的任务捞出来,但我只要第 1 条。"

对应到延迟队列的业务逻辑,参数拆解如下:

  • 0(时间下限) :因为时间戳是个递增的正整数,写 0 代表从最早、最老的时间开始算起,确保那些过去已经超时的任务不会被漏掉。
  • 1700000015(时间上限 = 当前时间戳) :这是最关键的限制。限制上限为"当前时间",意味着只有"执行时间 ≤ \le ≤ 当前时间"的任务才符合条件。那些还没到期的任务(比如要求在第 150 秒执行),因为分数大于 125,就会被直接过滤掉。
  • LIMIT 0 1(数量限制) :类似于 SQL 的 LIMIT 1。意思是虽然满足到期条件的可能有很多条,但我一次只取最该执行的那 1 条。这能有效防止高并发下多个消费者同时抢到大量重复任务。

四、 生产环境必须注意的"隐患"

在了解了 ZRANGEBYSCORE 的作用后,你会发现一个问题:它只是把数据"读"了出来,但并没有从 Redis 里"删掉"

如果两个消费者同时执行了上面那条命令,它们会同时拿到 order_id_10086,这就导致了重复消费

工业级的解决方案:
  1. Lua 脚本(推荐) :将 ZRANGEBYSCORE(查询)和 ZREM(删除)打包写进一个 Lua 脚本中。因为 Redis 执行 Lua 脚本是原子的,能够保证"谁先查到,谁就立马删掉",别人绝对抢不走。
  2. ZPOPMIN 命令(Redis 5.0+):直接弹出队列中分数最小的元素。可以先通过该命令弹出,并在代码中判断弹出的那个值是否小于当前时间,如果没到期再塞回去(或者配合其他逻辑),从而避免了重复消费。
相关推荐
渣渣盟3 小时前
当 Redis 写入成为性能瓶颈时,如何利用 异步批量 Sink 将吞吐量从 1w QPS 提升到 10w+?
数据库·redis·php
MC皮蛋侠客6 小时前
Redis 系列(六):过期、内存淘汰与内存优化——让 Redis 活得更久
数据库·redis·bootstrap
MC皮蛋侠客8 小时前
Redis 系列(七):事务、Pipeline 与 Lua 脚本——从原子性到 Functions
数据库·redis·lua
MC皮蛋侠客8 小时前
Redis 系列(八):缓存设计模式与一致性——从 Cache Aside 到防雪崩
redis·缓存·设计模式
渣渣盟8 小时前
当 Redis 写入不再是瓶颈后,Flink 任务的反压可能来自哪里?如何系统性地定位和解决 Flink 反压问题?
数据库·redis·flink
仍然.8 小时前
服务端高并发分布式结构演进之路
大数据·数据库·redis
MC皮蛋侠客8 小时前
Redis 系列(三):底层实现(一)——对象系统、SDS 与 dict
数据库·redis·缓存
渣渣盟9 小时前
当 Redis 集群发生主从切换(Failover)时,Flink 任务会崩溃吗?如何利用 Sentinel 实现高可用?
redis·flink·sentinel
海上小飞龙1 天前
Redis 分布式锁原理:从 SET NX EX 到 Redisson 看门狗
数据库·redis·分布式
万物皆字节1 天前
【避坑】使用CacheEvict需要注意的坑
redis