zset实现延迟队列
文章目录一、 核心设计思想什么是 ZSet 延迟队列二、 运行流程存 与 取1. 生产者存入任务投递消息2. 消费者取出任务轮询消费三、 深度理解ZRANGEBYSCORE 在这里起什么作用四、 生产环境必须注意的“隐患”工业级的解决方案将利用 Redis 的Sorted SetZSet实现延迟队列的核心逻辑与ZRANGEBYSCORE命令的作用合并我们可以用“存”和“取”两个动作来完整还原这个方案。一、 核心设计思想什么是 ZSet 延迟队列Redis 的 ZSet 是一个有序集合。它里面的每一条数据都会关联一个分数ScoreRedis 会自动根据分数从小到大给数据排好序。在延迟队列场景中我们做了一个巧妙的映射数据内容Member代表你要处理的任务比如订单 IDorder_id_10086。分数Score代表这个任务的具体执行时间戳即当前时间戳 延迟秒数。因为 ZSet 会自动排序所以最先到期的任务永远排在队列的最前面。二、 运行流程存 与 取1. 生产者存入任务投递消息当用户下单后系统需要开启一个“10秒后未支付自动取消订单”的延迟任务。假设当前时间戳是1700000000。生产者计算出执行时间1700000000 10 1700000010。然后使用ZADD命令把任务塞进名为delay_queue的 ZSet 中ZADD delay_queue1700000010order_id_100862. 消费者取出任务轮询消费消费者后台线程会像一个定时闹钟一样每隔 1 秒去 Redis 里巡检一次看看有没有到期的任务。它使用的核心命令就是ZRANGEBYSCORE。假设现在时间走到了1700000015距离下单过去了 15 秒消费者发起查询ZRANGEBYSCORE delay_queue01700000015LIMIT01三、 深度理解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直接弹出队列中分数最小的元素。可以先通过该命令弹出并在代码中判断弹出的那个值是否小于当前时间如果没到期再塞回去或者配合其他逻辑从而避免了重复消费。