Redis 实现延迟队列
Redis 做延迟队列有两种方案:
- Redis 过期时间监听
- Redisson 内置的延迟队列
Redis 过期事件监听实现延迟队列
Redis 有发布订阅(pub/sub)功能,在 pub/sub,引入了 channel(频道)
,类似于消息队列的 topic (主题)
pub/sub 涉及发布者publisher
和 订阅者 subscriber
两个角色
- 发布者通过
publish
投递给指定的 channel - 订阅者通过 subscribe 订阅一个或多个的 channel
生产者指定消息到 channel,消费者订阅对应的 channel
Redis 中有很多默认的 channel,这些 channel 是由 Redis 本身向它们发送消息,__keyevent@0__:expired
就是一个默认的 channel,负责监听 key 的过期事件
当一个 key 过期之后,Redis 会发布一个 key 过期的事件到这个 channel 中
只需要监听这个 channel,就可以拿到过期的 key 的消息,进而实现了延迟队列
缺点 1:时效性差
过期事件是在 Redis 删除 key 时发布的,不是设置的过期时间到的时候直接发布
过期删除策略有 2 个
- 惰性删除,对 CPU 友好,但是可能导致过期的 key 没有删除
- 定期删除,每隔一段时间抽取一批 key 执行删除,对内存友好
Redis 采用的是 定期删除+惰性删除,因此会存在 key 过期了,但是事件还未发布
缺点2:丢消息
Redis 中消息不支持持久化,当 channel 没有订阅者时,消息会被直接丢弃
缺点3:多服务实例下存在消息重复问题
Redis 的消息队列只有广播模式,所有订阅指定 channel 的实例全都能收到消息,需要处理重复消费问题
Redisson 内置的延迟队列实现
Redisson的延迟队列 RDelayedQueue 是基于 ZSet 实现的,需要执行延迟的任务插入到 ZSet 中,并给他们设置相应的过期时间作为分数
使用 zrangebyscore
命令扫描 ZSet 中过期的元素,然后将这些过期元素从 ZSet 中移除,并将它们加入到就绪消息列表中。就绪消息列表是一个阻塞队列,有消息进入就会被监听到。这样做可以避免对整个 ZSet 进行轮询,提高了执行效率
优势:
- 减少了丢消息的可能:DelayedQueue 中的消息会被持久化,即使Redis宕机了,根据持久化机制,也只可能丢失一点消息,影响不大。也可以使用扫描数据库的方法作为补偿机制。
- 消息不存在重复消费问题:每个客户端都是从同一个目标队列中获取任务的,不存在重复消费的问题。