在业务开发中,经常遇到需要延迟执行任务的场景,例如订单30分钟未支付自动取消、会议提醒等。使用Redis实现延迟队列是常见的解决方案。以下是三种主流实现方案的详细操作与对比。
方案一:使用有序集合
这是最经典且最容易理解的方案。利用Redis的 ZSET 数据结构,将任务执行时间戳作为 score,任务内容作为 member。
-
添加延迟任务。
使用ZADD命令将任务加入队列,score设置为当前时间戳加上延迟秒数。ZADD delay_queue 1715629200 "order:12345"上述指令表示在
1715629200(对应的时间点)处理订单order:12345。 -
轮询获取到期任务。
启动一个后台线程,每隔1秒(根据业务精度要求调整)执行以下逻辑。
使用ZRANGEBYSCORE命令查询score小于等于当前时间戳的任务。ZRANGEBYSCORE delay_queue -inf (current_timestamp) -
原子性地处理并移除任务。
为了防止并发环境下重复处理同一个任务,必须将“查询”和“删除”操作合并。编写一个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可以发送事件通知。我们可以利用这一机制实现延迟队列。
-
开启Key过期通知配置。
修改redis.conf配置文件,将notify-keyspace-events的值设置为Ex(表示监听键过期事件)。notify-keyspace-events Ex重启Redis服务使配置生效。
-
订阅过期频道。
在消费者服务中,订阅__keyevent@0__:expired频道(0代表数据库0号库)。SUBSCRIBE __keyevent@0__:expired -
设置带过期时间的Key。
当需要延迟任务时,将任务ID或内容存入一个Key,并设置对应的TTL(生存时间)。SETEX order:cancel:12345 1800 "payload"该指令表示
order:cancel:12345这个Key将在1800秒(30分钟)后过期,此时消费者会收到通知。
方案三:使用Redis Stream(推荐)
Redis 5.0 引入了 Stream 数据结构,它提供了类似于 Kafka 的消息队列功能,支持消费者组,是实现高可靠延迟队列的最佳方案。
-
创建消费者组。
首先确保Stream存在,然后创建消费者组,用于处理挂起(Pending)的消息。XGROUP CREATE delay_stream mygroup 0 MKSTREAM -
生产者写入消息。
使用XADD写入消息。为了实现延迟,可以直接利用XADD的MAXLEN或业务层控制,或者更巧妙地,配合XREADGROUP的BLOCK特性进行“空闲等待”,但精准延迟通常仍需结合ZSET或利用 Stream 的 ID 内部时间戳进行过滤。
更通用的做法是:使用XADD写入消息,消息体中包含期望执行的时间戳。 -
消费者读取并处理消息。
消费者使用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。
以下是混合模式下的消费者处理流程逻辑:
- 步骤A:任务到期时,从
方案对比
下表对三种方案的核心维度进行对比。
| 维度 | 方案一 (ZSET + Lua) | 方案二 (Key过期通知) | 方案三 (Stream 混合模式) |
|---|---|---|---|
| 实现复杂度 | 低(仅需ZSET和Lua) | 低(配置+订阅) | 中(需结合ZSET和Stream) |
| 精度控制 | 较高(取决于轮询间隔) | 较低(取决于Redis内部调度) | 高(取决于ZSET轮询) |
| 可靠性 | 中(宕机可能丢失未轮询任务) | 低(事件可能丢失,不保证送达) | 高(Stream支持持久化和ACK) |
| 消息回溯 | 不支持 | 不支持 | 支持(可读取历史消息) |
| 集群支持 | 支持(需考虑分片逻辑) | 支持 | 支持(Stream原生支持分区) |
| 适用场景 | 普通业务、非极高可靠性要求 | 简单通知、允许丢失的场景 | 核心业务、要求高可靠和可回溯 |
选择建议:
- 对于并发量不大、且允许极少量误差的普通业务,优先选择方案一,开发成本最低,维护简单。
- 对于核心业务、订单取消等绝对不能丢的场景,强烈推荐方案三(ZSET作为延迟表,Stream作为消息表)。
- 尽量避免使用方案二,除非对可靠性要求极低。因为Redis不保证过期事件的实时送达(在负载高时可能延迟或丢失事件),且断开重连期间的事件会永久丢失。

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