Redis实现消息队列之生产消费模式

简单的介绍下消息队列,使用消息队列首先我们得有一个队列,那么这个队列之前讲过就是先进先出的一个数据结构;那么有了队列以后我们还需要有人在队列里面放东西,那么这个放东西的人我们称之为生产者;有了生产者对应的需要一个消费者,没有消费者这个队列满了就会溢出。

简单队列实现

那么我们Redis刚好有一个数据类型符合这个就是List。list可以实现队列(先进先出)和栈(先进后出),那么这个list又有两种插入数据的方式:头插法和尾插法。所以我们今天使用的结构是队列,使用尾插法,关键的命令有rpush(尾插)和lpop(头部获取)。如下图所示:

> rpush squeue zhangsan lisi mango    #插入队列(integer) 3> lpop squeue    #获取队列"zhangsan"> rpush squeue wangwu(integer) 3> lpop squeue"lisi"> lpop squeue"mango"> lpop squeue"wangwu"> lpop squeue    #空队列(nil)

上面是rpush/lpop结合使用的例子。还可以使用lpush/rpop结合使用,效果是一样的。

注意:我们这里思考一个问题,在咱们实际开发中我们获取队列数据的时候如果这个队列里面没有任何值了,我们会一直pop,这样我们的程序出现了一个死循环,而且此时的redis会不断的处理服务器的pop指令使之内存增高。

此时mango灵光一闪,每次pop的时候判断,如果队列有值我们获取这个值进行操作,如果队列是空队列,那么我们此时休息三秒钟再请求,emmm,加鸡腿。

> lpop squeue    #空队列(nil).......sleep 3s old> lpop squeue...

队列阻塞

通过后端程序控制redis服务器休息时间是一个好办法,但是此处有一个问题,如果说服务器在休眠的时候队列突然进来一个值,而此时需要及时反应获取这个值该如何实现呢?

当然,redis早就考虑到这个问题,so提供了一个叫做队列阻塞读,其命令blpop和brpop,就是lpop和rpop的阻塞读方法

blpop [第一个参数:key]... [第二个参数:time]key可以有多个key等待time就是阻塞时间,单位默认为秒

嗯,看似完美的解决了上面的方法

问题又来了,如果说此处设置的时间稍微长一点,阻塞请求的客户端连接再多一点那么会出现下一个问题,那就是空闲连接问题如果线程一直阻塞在哪里,Redis的客户端连接就成了闲置连接,闲置过久,服务器一般会主动断开连接,减少闲置资源占用。这个时候blpop/brpop会抛出异常来。

所以,鱼与熊掌不可兼得,开发者当注意此处需要捕获异常,然后重新请求。

延迟队列

上面我们介绍了阻塞队列,深知空闲连接会被回收出现异常问题,那么我们可不可以实现延迟队列,我提前一段时间比如5秒获取队列中的元素,当元素记录的时间到达了我再去执行这个值,然后又提前5秒时间去获取队列中的值,依次反复。即可保证我在某一时刻去执行队列中对应时间的值。

我们来分析,首先保证队列是一个有序的我们才能依次执行,这里我们使用ZSet因为它带有排序且不重复,保证客户端没有提交重复数据,那么值保证了,这个排序如何设计呢?我们不能使用'yyyyMMddHHmmssSSS'这种,第一个不适应这个排序类型可能会超出,第二就是每次转换会带来运算转换的消耗,所有这里我们使用时间戳。那么zset提供一个获取某段存在数据指令zrangebyscore,这个指令能够获取到我们想要的数据,然后通过zrem删除zset里面的值即可完成消费。

####假设当前时间戳是0> zadd dqueue 4 zhangsan 8 lisi 12 mango    #添加元素(integer) 3####提前获取后5秒的数据> zrangebyscore dqueue -inf 5 withscores    #提前获取5秒内的数据1) "zhangsan"    #值2) 4.0           #当前排序索引> zrem dqueue zhangsan     #消费后删除元素1####当前时间戳来到了5秒,提前5秒获取就加上这个> zrangebyscore dqueue -inf 10 withscores    #获取10以内的数据,轮训调用1) "lisi"2) 8.0

写在最后

我们首先实现简单的生成消费模式,针对这种简单轮询我们通过有数据和无数据让这个轮询实现休息策略来优化,需求更改需要及时响应生产者的数据我们使用了阻塞队列来减少响应延时,通过分析我们又发现阻塞队列会被回收的问题,我们又双叒叕进行了优化,通过zset来实现延迟队列。

你以为这样就万无一失了,其实还存在一个问题那就是不能保证原子性,我们的zrangebyscore和zrem不能在同一原子内执行,这里只是针对的一个客户端,如果有多个客户端执行那就会出现多次消费问题,也就是资源争抢,这里我们可以使用lua脚本来执行保证原子性,即获取后删除,其值和时间戳保存到内存中,通过后端程序控制时间消费。当然,问题还会有的,我们在下一篇来讲解Redis消息队列的发布订阅模式。

 

一名正在抢救的coder

笔名:mangolove

CSDN地址:https://blog.csdn.net/mango_love

GitHub地址:https://github.com/mangoloveYu

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章