文章目录

Redis实现延迟队列的三种方案对比

发布于 2026-04-24 02:14:52 · 浏览 13 次 · 评论 0 条

在业务开发中,经常遇到需要延迟执行任务的场景,例如订单30分钟未支付自动取消、会议提醒等。使用Redis实现延迟队列是常见的解决方案。以下是三种主流实现方案的详细操作与对比。


方案一:使用有序集合

这是最经典且最容易理解的方案。利用Redis的 ZSET 数据结构,将任务执行时间戳作为 score,任务内容作为 member

  1. 添加延迟任务。
    使用 ZADD 命令将任务加入队列,score 设置为当前时间戳加上延迟秒数。

    ZADD delay_queue 1715629200 "order:12345"

    上述指令表示在 1715629200(对应的时间点)处理订单 order:12345

  2. 轮询获取到期任务。
    启动一个后台线程,每隔1秒(根据业务精度要求调整)执行以下逻辑。
    使用 ZRANGEBYSCORE 命令查询 score 小于等于当前时间戳的任务。

    ZRANGEBYSCORE delay_queue -inf (current_timestamp)
  3. 原子性地处理并移除任务。
    为了防止并发环境下重复处理同一个任务,必须将“查询”和“删除”操作合并。编写一个Lua脚本,确保同一时间只有一个线程能成功拿到并移除该任务。

    -- Lua脚本内容
    local tasks = redis.call('ZRANGEBYSCORE', KEYS[1], '-inf', ARGV[1], 'LIMIT', 0, 1)
    if #tasks > 0 then
        redis.call('ZREM', KEYS[1], tasks[1])
        return tasks[1]
    else
        return nil
    end

    在代码中执行该脚本,传入 KEYS[1] 为队列名,ARGV[1] 为当前时间戳。


方案二:利用Key过期通知

Redis的Key带有过期特性,当Key过期时,Redis可以发送事件通知。我们可以利用这一机制实现延迟队列。

  1. 开启Key过期通知配置。
    修改 redis.conf 配置文件,将 notify-keyspace-events 的值设置为 Ex(表示监听键过期事件)。

    notify-keyspace-events Ex

    重启Redis服务使配置生效。

  2. 订阅过期频道。
    在消费者服务中,订阅 __keyevent@0__:expired 频道(0 代表数据库0号库)。

    SUBSCRIBE __keyevent@0__:expired
  3. 设置带过期时间的Key。
    当需要延迟任务时,将任务ID或内容存入一个Key,并设置对应的TTL(生存时间)。

    SETEX order:cancel:12345 1800 "payload"

    该指令表示 order:cancel:12345 这个Key将在1800秒(30分钟)后过期,此时消费者会收到通知。


方案三:使用Redis Stream(推荐)

Redis 5.0 引入了 Stream 数据结构,它提供了类似于 Kafka 的消息队列功能,支持消费者组,是实现高可靠延迟队列的最佳方案。

  1. 创建消费者组。
    首先确保Stream存在,然后创建消费者组,用于处理挂起(Pending)的消息。

    XGROUP CREATE delay_stream mygroup 0 MKSTREAM
  2. 生产者写入消息。
    使用 XADD 写入消息。为了实现延迟,可以直接利用 XADDMAXLEN 或业务层控制,或者更巧妙地,配合 XREADGROUPBLOCK 特性进行“空闲等待”,但精准延迟通常仍需结合 ZSET 或利用 Stream 的 ID 内部时间戳进行过滤。
    更通用的做法是:使用 XADD 写入消息,消息体中包含期望执行的时间戳。

  3. 消费者读取并处理消息。
    消费者使用 XREADGROUP 读取消息。为了实现延迟效果,消费者在读取到消息后,先判断消息中的执行时间戳。

    XREADGROUP GROUP mygroup consumer1 COUNT 1 BLOCK 5000 STREAMS delay_stream >

    如果时间未到,使用 XADD 将消息重新放入Stream尾部(或使用 XPENDING 查看但不ACK),或者更简单地,结合方案一的思路:
    实际上,Stream 本身不支持自动延迟投递。标准的“Stream实现延迟队列”通常是指:先放入 ZSET,时间到了后,转移到 Stream 中供消费者消费。这种混合模式结合了二者的优点。这里描述最直接的混合模式操作:

    • 步骤A:任务到期时,从 ZSET 取出任务。
    • 步骤B:将任务通过 XADD 写入 Stream
    • 步骤C:消费者监听 Stream

    以下是混合模式下的消费者处理流程逻辑:

graph TD A["ZSET 轮询线程"] -->|检测到时间到期| B["ZREM 获取任务"] B --> C["XADD 写入 Stream"] C --> D["Stream 消费者组"] D --> E["XREADGROUP 读取消息"] E -->|处理成功| F["XACK 确认"] E -->|处理失败| G["不 ACK, 留待重试"] F --> H["结束"] G --> H

方案对比

下表对三种方案的核心维度进行对比。

维度 方案一 (ZSET + Lua) 方案二 (Key过期通知) 方案三 (Stream 混合模式)
实现复杂度 低(仅需ZSET和Lua) 低(配置+订阅) 中(需结合ZSET和Stream)
精度控制 较高(取决于轮询间隔) 较低(取决于Redis内部调度) 高(取决于ZSET轮询)
可靠性 中(宕机可能丢失未轮询任务) 低(事件可能丢失,不保证送达) 高(Stream支持持久化和ACK)
消息回溯 不支持 不支持 支持(可读取历史消息)
集群支持 支持(需考虑分片逻辑) 支持 支持(Stream原生支持分区)
适用场景 普通业务、非极高可靠性要求 简单通知、允许丢失的场景 核心业务、要求高可靠和可回溯

选择建议

  1. 对于并发量不大、且允许极少量误差的普通业务,优先选择方案一,开发成本最低,维护简单。
  2. 对于核心业务、订单取消等绝对不能丢的场景,强烈推荐方案三(ZSET作为延迟表,Stream作为消息表)。
  3. 尽量避免使用方案二,除非对可靠性要求极低。因为Redis不保证过期事件的实时送达(在负载高时可能延迟或丢失事件),且断开重连期间的事件会永久丢失。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文