文章目录
-
-
- [一、 核心设计思想:什么是 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 里,把分数在 0 到 1700000015 之间的任务捞出来,但我只要第 1 条。"
对应到延迟队列的业务逻辑,参数拆解如下:
0(时间下限) :因为时间戳是个递增的正整数,写0代表从最早、最老的时间开始算起,确保那些过去已经超时的任务不会被漏掉。1700000015(时间上限 = 当前时间戳) :这是最关键的限制。限制上限为"当前时间",意味着只有"执行时间 ≤ \le ≤ 当前时间"的任务才符合条件。那些还没到期的任务(比如要求在第 150 秒执行),因为分数大于 125,就会被直接过滤掉。LIMIT 0 1(数量限制) :类似于 SQL 的LIMIT 1。意思是虽然满足到期条件的可能有很多条,但我一次只取最该执行的那 1 条。这能有效防止高并发下多个消费者同时抢到大量重复任务。
四、 生产环境必须注意的"隐患"
在了解了 ZRANGEBYSCORE 的作用后,你会发现一个问题:它只是把数据"读"了出来,但并没有从 Redis 里"删掉"。
如果两个消费者同时执行了上面那条命令,它们会同时拿到 order_id_10086,这就导致了重复消费。
工业级的解决方案:
- Lua 脚本(推荐) :将
ZRANGEBYSCORE(查询)和ZREM(删除)打包写进一个 Lua 脚本中。因为 Redis 执行 Lua 脚本是原子的,能够保证"谁先查到,谁就立马删掉",别人绝对抢不走。 ZPOPMIN命令(Redis 5.0+):直接弹出队列中分数最小的元素。可以先通过该命令弹出,并在代码中判断弹出的那个值是否小于当前时间,如果没到期再塞回去(或者配合其他逻辑),从而避免了重复消费。