Redis 延时队列详解

 更新时间:2026年06月30日 08:24:44   作者:用户307459698207  
本文主要介绍了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 nil
end
local 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 end
local task = tasks[1]
if redis.call('ZREM', KEYS[1], task) == 1 then
    return task
else
    return nil
end
LUA;
// 定时任务循环执行
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 end
if redis.call('ZREM', KEYS[1], tasks[1]) == 1 then
    return tasks[1]
end
return nil
LUA;
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 原子抢任务 = 可靠的延时队列

到此这篇关于Redis 延时队列详解的文章就介绍到这了,更多相关Redis 延时队列内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Redis都做了哪些加快速度的设计

    Redis都做了哪些加快速度的设计

    这篇文章主要介绍了Redis都做了哪些加快速度的设计的相关资料,需要的朋友可以参考下
    2021-02-02
  • redis配置认证密码的方法

    redis配置认证密码的方法

    这篇文章主要介绍了redis配置认证密码的方法,需要的朋友可以参考下
    2016-08-08
  • redis发布订阅模式的实现

    redis发布订阅模式的实现

    本文主要介绍了redis发布订阅模式,Redis 的 SUBSCRIBE 命令可以让客户端订阅任意数量的频道,文中通过示例代码介绍的非常详细,需要的朋友们下面随着小编来一起学习学习吧
    2024-04-04
  • redis+lua实现限流的项目实践

    redis+lua实现限流的项目实践

    redis有很多限流的算法(比如:令牌桶,计数器,时间窗口)等,在分布式里面进行限流的话,我们则可以使用redis+lua脚本进行限流,下面就来介绍一下redis+lua实现限流
    2023-10-10
  • redis内存空间效率问题的深入探究

    redis内存空间效率问题的深入探究

    redis缓存固然高效,可是它会占用我们系统中宝贵的内存资源,那该如何解决呢?这篇文章主要给大家介绍了关于redis内存空间效率问题的相关资料,需要的朋友可以参考下
    2021-05-05
  • Redis数据库的键管理示例详解

    Redis数据库的键管理示例详解

    这篇文章主要为大家介绍了Redis数据库的键管理示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-10-10
  • Redis 延时任务实现及与定时任务区别详解

    Redis 延时任务实现及与定时任务区别详解

    这篇文章主要为大家介绍了Redis 延时任务实现及与定时任务区别详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-06-06
  • Redis Hash序列化存储的问题及解决方案

    Redis Hash序列化存储的问题及解决方案

    这篇文章主要介绍了Redis Hash序列化存储的问题及解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-11-11
  • Redis 5.05 单独模式安装及配置方法

    Redis 5.05 单独模式安装及配置方法

    这篇文章主要介绍了Redis 5.05 单独模式安装,文中通过代码给大家介绍了Redis 5.0.5 单节点 安装配置方法,需要的朋友可以参考下
    2019-10-10
  • Redis集群利用Redisson实现分布式锁方式

    Redis集群利用Redisson实现分布式锁方式

    这篇文章主要介绍了Redis集群利用Redisson实现分布式锁方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-05-05

最新评论