Redis ZSet 实现延迟队列
在实际开发中,经常会遇到需要延迟执行任务的场景,例如:
- 订单超时自动取消
- 支付超时关闭订单
- 定时发送通知
- 自动释放库存
对于这类需求,Redis 的 ZSet(有序集合) 是一种简单高效的实现方案。
实现原理
ZSet 中每个元素都有一个 score,Redis 会按照 score 自动排序。
延迟队列可以利用这一特性:
- score:任务执行时间戳
- member:任务内容
例如:
text
delay_queue
1718857800 -> order_1001
1718857860 -> order_1002
1718857920 -> order_1003
当当前时间达到对应时间戳时,即可消费任务。
添加任务
将未来执行时间作为 score 存入 ZSet:
go
executeTime := time.Now().
Add(30 * time.Minute).
Unix()
rdb.ZAdd(ctx, "delay_queue", redis.Z{
Score: float64(executeTime),
Member: "order_1001",
})
获取到期任务
查询所有已到期任务:
go
now := time.Now().Unix()
tasks, err := rdb.ZRangeByScore(
ctx,
"delay_queue",
&redis.ZRangeBy{
Min: "-inf",
Max: strconv.FormatInt(now, 10),
},
).Result()
并发问题
如果多个消费者同时查询:
text
消费者A 获取 order_1001
消费者B 获取 order_1001
可能导致任务被重复执行。
因此需要保证:
text
查询任务
+
删除任务
是一个原子操作。
Lua脚本实现原子消费
lua
local result = redis.call(
'ZRANGEBYSCORE',
KEYS[1],
'-inf',
ARGV[1],
'LIMIT',
0,
1
)
if #result == 0 then
return nil
end
redis.call('ZREM', KEYS[1], result[1])
return result[1]
对应 Go 调用:
go
result, err := luaScript.Run(
ctx,
rdb,
[]string{"delay_queue"},
time.Now().Unix(),
).Result()
这样可以避免任务被多个消费者重复消费。
典型应用
订单超时关闭
text
创建订单
↓
30分钟后检查
↓
未支付则自动关闭
用户注册提醒
text
用户注册
↓
24小时后
↓
发送提醒消息
优缺点
优点
- 实现简单
- 性能高
- 无需引入额外MQ
- 适合中小型延迟任务场景
缺点
- 不支持消息确认机制
- 消费失败需要自行重试
- 不适合超大规模任务
总结
Redis ZSet 延迟队列的核心思想非常简单:
text
score = 执行时间戳
member = 任务数据
通过 ZADD 存储任务,ZRANGEBYSCORE 获取到期任务,再结合 Lua 脚本保证原子消费,即可快速实现一个轻量级延迟队列。
对于订单超时、定时通知等业务场景,ZSet 是一种非常实用的解决方案。