背景
前段时间有个小项目需要使用延迟任务,谈到延迟任务,我脑子第一时间一闪而过的就是使用消息队列来做,比如RabbitMQ的死信队列又或者RocketMQ的延迟队列,但是奈何这是一个小项目,并没有引入MQ,我也不太想因为一个延迟任务就引入MQ,增加系统复杂度,所以这个方案直接就被pass了。
虽然基于MQ这个方式走不通了,但是这个项目中使用到Redis,所以我就想是否能够使用Redis来代替MQ实现延迟队列的功能,于是我就查了一下有没有现成可用的方案,别说,还真给我查到了两种方案,并且我还仔细研究对比了这两个方案,发现要想很好的实现延迟队列,并不简单。
监听过期key
基于监听过期key的方式来实现延迟队列是我查到的第一个方案,为了弄懂这个方案实现的细节,我还特地去扒了扒官网,还真有所收获
1、Redis发布订阅模式
一谈到发布订阅模式,其实一想到的就是MQ,只不过Redis也实现了一套,并且跟MQ贼像,如图:
图中的channel的概念跟MQ中的topic的概念差不多,你可以把channel理解成MQ中的topic。
生产者在消息发送时需要到指定发送到哪个channel上,消费者订阅这个channel就能获取到消息。
2、keyspace notifications
在Redis中,有很多默认的channel,只不过向这些channel发送消息的生产者不是我们写的代码,而是Redis本身。当消费者监听这些channel时,就可以感知到Redis中数据的变化。
这个功能Redis官方称为keyspace notifications,字面意思就是键空间通知。
这些默认的channel被分为两类:
-
以
__keyspace@<db>__:
为前缀,后面跟的是key的名称,表示监听跟这个key有关的事件。举个例子,现在有个消费者监听了
__keyspace@0__:sanyou
这个channel,sanyou就是Redis中的一个普通key,那么当sanyou这个key被删除或者发生了其它事件,那么消费者就会收到sanyou这个key删除或者其它事件的消息 -
以
__keyevent@<db>__:
为前缀,后面跟的是消息事件类型,表示监听某个事件同样举个例子,现在有个消费者监听了
__keyevent@0__:expired
这个channel,代表了监听key的过期事件。那么当某个Redis的key过期了(expired),那么消费者就能收到这个key过期的消息。如果把expired换成del,那么监听的就是删除事件。具体支持哪些事件,可从官网查。
上述db是指具体的数据库,Redis不是默认分为16个库么,序号从0-15,所以db就是0到15的数字,示例中的0就是指0对应的数据库。
3、延迟队列实现原理
通过对上面的两个概念了解之后,应该就对监听过期key的实现原理一目了然了,其实就是当这个key过期之后,Redis会发布一个key过期的事件到__keyevent@<db>__:expired
这个channel,只要我们的服务监听这个channel,那么就能知道过期的Key,从而就算实现了延迟队列功能。
所以这种方式实现延迟队列就只需要两步:
- 发送延迟任务,key是延迟消息本身,过期时间就是延迟时间
- 监听
__keyevent@<db>__:expired
这个channel,处理延迟任务
4、demo
好了,基本概念和核心原理都说完了之后,又到了show me the code环节。
好巧不巧,Spring已经实现了监听__keyevent@*__:expired
这个channel这个功能,__keyevent@*__:expired
中的*
代表通配符的意思,监听所有的数据库。
所以demo写起来就很简单了,只需3步即可
引入pom
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring