ZSet实现延迟队列,完美解决延迟任务。

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 是一种非常实用的解决方案。