一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

详解Redis延时队列

时间:2026-07-04 10:39:47 编辑:袖梨 来源:一聚教程网

一、什么是延时队列

普通队列: 消息一到就消费

Redis延时队列详解

延时队列: 消息到了先放着,到指定时间再消费

普通队列:  [消息] → 立即消费

延时队列:  [消息] → 等 30 分钟 → 到期 → 消费

场景举例:

  • 下单 30 分钟未支付,自动取消
  • 红包 24 小时未领取,自动退回
  • 会议开始前 5 分钟,发送提醒
  • 7 天后自动确认收货

二、为什么用 ZSet 实现

数据类型结构能做延时队列吗
List按插入排序❌ 不支持按时间排序
Set无序❌ 没法指定执行时间
ZSet按 score 排序✅ score 存到期时间戳
# ZSet 天然适合:ZADD delay_queue 1718000000 "task_001"   # score=到期时间戳ZADD delay_queue 1718000300 "task_002"   # 自动按时间排序

三、核心流程

生产者                     Redis ZSet                      消费者(定时任务)──────                    ──────────                      ──────────────XADD delay_q              score  = 到期时间戳score=到期时间            member = 任务数据                          ┌─────────────────┐                          │ 1718000000 task1 │  ← 最早到期                          │ 1718000300 task2 │                          │ 1718000600 task3 │                          └─────────────────┘                                  ↓                          ZRANGEBYSCORE 0 当前时间                          取到 task1(已到期)                                  ↓                          执行 task1 → ZREM 删除

四、基础实现(有并发问题)

// 生产者:投递延时任务$redis->zAdd('delay:orders', time() + 1800, json_encode([    'order_id' => 12345,    'action'   => 'auto_cancel',]));// 消费者:每秒轮询到期任务(有 BUG 的版本)$now = time();$tasks = $redis->zRangeByScore('delay:orders', 0, $now, ['limit' => [0, 1]]);if ($tasks) {    $task = $tasks[0];    // ⚠️ 这里有 BUG!如果同时多个消费者拿到同一条    $redis->zRem('delay:orders', $task);    processTask($task);}

问题在哪? ZRANGEBYSCOREZREM 是分开的,多个消费者可能同时拿到同一条任务!

五、Lua 脚本原子化(解决并发)

-- 原子操作:查出到期任务 + 立刻删除 + 返回local tasks = redis.call('ZRANGEBYSCORE', KEYS[1], 0, ARGV[1], 'LIMIT', 0, 1)if #tasks == 0 then    return nilendlocal task = tasks[1]local removed = redis.call('ZREM', KEYS[1], task)if removed == 1 then    return task       -- 删除成功,返回任务else    return nil        -- 被别的消费者抢了end

PHP 端调用:

$lua = <<<'LUA'local tasks = redis.call('ZRANGEBYSCORE', KEYS[1], 0, ARGV[1], 'LIMIT', 0, 1)if #tasks == 0 then return nil endlocal task = tasks[1]if redis.call('ZREM', KEYS[1], task) == 1 then    return taskelse    return nilendLUA;// 定时任务循环执行while (true) {    $task = $redis->eval($lua, ['delay:orders', time()], 1);    //                                      ↑ KEYS 部分       ↑ key数量    if ($task) {        $data = json_decode($task, true);        echo "处理任务: {$data['order_id']}n";        processTask($data);    } else {        sleep(1);  // 没任务就等一下    }}

六、完整实战:30 分钟未支付自动取消

<?php// ====== 生产者(下单时) ======function createOrder($orderId) {    $redis = new Redis();    $redis->connect('127.0.0.1', 6380);    // 1. 创建订单...    // 2. 投递延时任务:30分钟后自动取消    $delayAt = time() + 1800;  // 30分钟    $task = json_encode([        'order_id' => $orderId,        'action'   => 'auto_cancel',        'create_at'=> date('Y-m-d H:i:s'),    ]);    $redis->zAdd('delay:orders', $delayAt, $task);    echo "订单 {$orderId} 已创建,30分钟后未支付将自动取消n";}// ====== 消费者(定时脚本) ======$lua = <<<'LUA'local tasks = redis.call('ZRANGEBYSCORE', KEYS[1], 0, ARGV[1], 'LIMIT', 0, 1)if #tasks == 0 then return nil endif redis.call('ZREM', KEYS[1], tasks[1]) == 1 then    return tasks[1]endreturn nilLUA;while (true) {    $task = $redis->eval($lua, ['delay:orders', time()], 1);    if ($task) {        $data = json_decode($task, true);        // 检查订单是否已支付        $order = getOrder($data['order_id']);        if ($order['status'] === 'unpaid') {            cancelOrder($data['order_id']);            echo "⏰ 订单 {$data['order_id']} 超时未支付,已自动取消n";        } else {            echo "✓ 订单 {$data['order_id']} 已支付,跳过n";        }    } else {        sleep(1);    }}

七、ZSet 延时队列 vs 其他方案

方案原理优点缺点
ZSetscore=时间戳,轮询取简单,Redis 自带需要轮询,精度秒级
Redis 过期回调key 过期触发通知不用轮询通知不可靠,可能丢失
RabbitMQ 延时插件消息自带 TTL + 死信队列专业可靠需要额外装插件
数据库轮询定时扫表实现简单大量数据时很慢

八、ZSet 的 score 谁来赋值的

ZADD key score member          ↑     你自己指定的ZADD delay_queue 1718000000 "task_001"#                 ↑    时间戳就是 score,你自己算的#                 score 决定了 ZSet 里的排序

排序规则: score 越小越靠前,所以过期时间越早的排最前面。

九、Set vs ZSet 能不能做延时队列

SetZSet
有序吗❌ 无序✅ 按 score 排序
能查到期的吗ZRANGEBYSCORE 0 now
能做延时队列吗不行可以

延时队列的核心需求是"按时间排序、查到期任务",只有 ZSet 能做到。

十、面试常问

Q: 延时队列为什么不用 List?

List 只能头进头出,无法按时间排序,不知道哪些消息到期了。

Q: 轮询会不会性能差?

单次 ZRANGEBYSCORE + ZREM 是 O(log N),每秒轮询对 Redis 压力很小。10万条延时任务也没问题。

Q: 很大量级的延时任务怎么办?

  1. 用多个 ZSet key 分桶(按分钟/小时)
  2. 每个桶一个消费者线程
  3. 配合 Redis Cluster 分片

Q: 消息丢了怎么办?

Redis 纯内存的话宕机会丢。重要业务建议:

  • AOF 持久化
  • 订单状态双写(Redis + DB),定时任务扫表兜底

Q: score 存毫秒级时间戳可以吗?

可以,ZSet 的 score 是 double 浮点数,毫秒时间戳完全放得下。

核心记住:ZSet 的 score 排序 + Lua 原子抢任务 = 可靠的延时队列

热门栏目