Redis实现消息队列&发布/订阅模式使用
- Redis的列表类型键可以用来实现队列,并且支持阻塞式读取,可以很容易的实现一个高性能的优先队列。
- 同时在更高层面上,Redis还支持"发布/订阅"的消息模式,可以基于此构建一个聊天系统。
redis的列表类型天生支持用作消息队列(类似于MQ的队列模型--任何时候都可以消费,一条消息只能消费一次)
在Redis中,List类型是按照插入顺序排序的字符串链表。和数据结构中的普通链表一样,我们可以在其头部(left)和尾部(right)添加新的元素。在插入时,如果该键并不存在,Redis将为该键创建一个新的链表。与此相反,如果链表中所有的元素均被移除,那么该键也将会被从数据库中删除。List中可以包含的最大元素数量是4294967295。
从元素插入和删除的效率视角来看,如果我们是在链表的两头插入或删除元素,这将会是非常高效的操作,即使链表中已经存储了百万条记录,该操作也可以在常量时间内完成。然而需要说明的是,如果元素插入或删除操作是作用于链表中间,那将会是非常低效的。
redis中简单的操作list,简单的在命令行操作实现队列
(1)从左向右插入,从右向左弹出:
执行完 lpush mylist a b c d 之后数据结构如下:(满足先进先出的队列模式)
执行完第一次:rpop mylist之后数据结构如下:
(2)从右向左插入,从左向右弹出:
执行完:rpush mylist2 a b c d之后的数据结构如下
第一次执行完 lpop mylist2 之后数据结构如下:(满足先进先出的队列模式)
但上述例子中消息消费者有一个问题存在,即需要不停的调用rpop方法查看List中是否有待处理消息。每调用一次都会发起一次连接,这会造成不必要的浪费。
也就是上面的操作需要一直调用rpop命令或者lpop命令才可以实现不停的监听且消费消息。为了解决这一问题,redis提供了阻塞命令 brpop和blpop。
补充:brpop和blpop实现阻塞读取(重要)
brpop命令可以接收多个键,其完整的命令格式为 BRPOP key [key ...] timeout,如:brpop key1 0。意义是同时检测多个键,如果所有键都没有元素则阻塞,如果其中一个有元素则从该键中弹出该元素(会按照key的顺序进行读取,可以实现具有优先级的队列)
执行brpop命令:(会返回读取的key和value,第一个是返回的key,第二个是value)
lpush queue1 1 2
lpush queue2 3 4
127.0.0.1:6379> brpop queue1 queue2 2
1) "queue1" 2) "1"
也就是brpop会阻塞队列,并且每次也是弹出一个消息,如果没有消息会阻塞。当弹出一个元素之后就会解除阻塞
如果多个键都有元素则按照从左到右读取第一个键中的一个元素,借此特性可以实现区分优先级的任务队列
补充:B|RPOPLPUSH source destination timeout
原子性的从与source键关联的链表尾部弹出一个元素,同时再将弹出的元素插入到与destination键关联的链表的头部。如果source键不存在,该命令将返回nil,同时不再做任何其它的操作了。如果source和destination是同一个键,则相当于原子性的将其关联链表中的尾部元素移到该链表的头部。如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止,返回弹出和插入的元素。
Redis链表经常会被用于消息队列的服务,以完成多程序之间的消息交换。假设一个应用程序正在执行LPUSH操作向链表中添加新的元素,我们通常将这样的程序称之为"生产者(Producer)",而另外一个应用程序正在执行B|RPOP操作从链表中取出元素,我们称这样的程序为"消费者(Consumer)"。如果此时,消费者程序在取出消息元素后立刻崩溃,由于该消息已经被取出且没有被正常处理,那么我们就可以认为该消息已经丢失,由此可能会导致业务数据丢失,或业务状态的不一致等现象的发生。然而通过使用B|RPOPLPUSH命令,消费者程序在从主消息队列中取出消息之后再将其插入到备份队列中,直到消费者程序完成正常的处理逻辑后再将该消息从备份队列中删除。同时我们还可以提供一个守护进程,当发现备份队列中的消息过期时,可以重新将其再放回到主消息队列中,以便其它的消费者程序继续处理。
发布/订阅模式(类似于MQ的主题模式-只能消费订阅之后发布的消息,一个消息可以被多个订阅者消费)
Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。Redis 客户端可以订阅任意数量的频道。
"发布/订阅"模式包含两种角色,分别是发布者和订阅者。订阅者可以订阅一个或者多个频道(channel),而发布者可以向指定的频道(channel)发送消息,所有订阅此频道的订阅者都会收到此消息。
图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系:
当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端:
(1)发布消息
发布者发布消息的命令是 publish,用法是 publish channel message,如向 channel1.1说一声hi
127.0.0.1:6379> publish channel:1 hi (integer) 0
这样消息就发出去了。返回值表示接收这条消息的订阅者数量。发出去的消息不会被持久化,也就是有客户端订阅channel:1后只能接收到后续发布到该频道的消息,之前的就接收不到了。
(2)订阅频道
订阅频道的命令是 subscribe,可以同时订阅多个频道,用法是 subscribe channel1 [channel2 ...],例如新开一个客户端订阅上面频道:(不会收到消息,因为不会收到订阅之前就发布到该频道的消息)
127.0.0.1:6379> subscribe channel:1 Reading messages... (press Ctrl-C to quit) 1) "subscribe" 2) "channel:1" 3) (integer) 1
执行上面命令客户端会进入订阅状态,处于此状态下客户端不能使用除subscribe、unsubscribe、psubscribe和punsubscribe这四个属于"发布/订阅"之外的命令,否则会报错。
进入订阅状态后客户端可能收到3种类型的回复。每种类型的回复都包含3个值,第一个值是消息的类型,根据消类型的不同,第二个和第三个参数的含义可能不同。
消息类型的取值可能是以下3个:
- subscribe:表示订阅成功的反馈信息。第二个值是订阅成功的频道名称,第三个是当前客户端订阅的频道数量。
- message:表示接收到的消息,第二个值表示产生消息的频道名称,第三个值是消息的内容。
- unsubscribe:表示成功取消订阅某个频道。第二个值是对应的频道名称,第三个值是当前客户端订阅的频道数量,当此值为0时客户端会退出订阅状态,之后就可以执行其他非"发布/订阅"模式的命令了。
除了可以使用subscribe命令订阅指定的频道外,还可以使用psubscribe命令订阅指定的规则。规则支持通配符格式。
命令格式为:psubscribe pattern [pattern ...] 订阅多个模式的频道。如: psubscribe c? b* d?*
通配符中?表示1个占位符,*表示任意个占位符(包括0),?*表示1个以上占位符。
注意:
(1)使用psubscribe命令可以重复订阅同一个频道,如客户端执行了psubscribe c? c?*。这时向c1发布消息客户端会接受到两条消息,而同时publish命令的返回值是2而不是。.同样的,如果有另一个客户端执行了subscribe c1 和psubscribe c?*的话,向c1发送一条消息该客户顿也会受到两条消息(但是是两种类型:message和pmessage),同时publish命令也返回2.
(2)punsubscribe命令可以退订指定的规则,用法是: punsubscribe [pattern [pattern ...]],如果没有参数则会退订所有规则。
(3)使用punsubscribe只能退订通过psubscribe命令订阅的规则,不会影响直接通过subscribe命令订阅的频道;同样unsubscribe命令也不会影响通过psubscribe命令订阅的规则。另外需要注意punsubscribe命令退订某个规则时不会将其中的通配符展开,而是进行严格的字符串匹配,所以punsubscribe * 无法退订c*规则,而是必须使用punsubscribe c*才可以退订。
至此实现了两种方式的消息队列:
redis自带的list类型(lpush和rpop或者brpop,rpush和lpop或者blpop)---blpop和brpop是阻塞读取。
"发布/订阅"模式(publish channel message 和 subscribe channel [channel ...] 或者 psubscribe pattern [pattern ...] 通配符订阅多个频道)
1.发布订阅执行订阅之后该线程处于阻塞状态,线程不会终止,如果终止线程需要退订,unsubscribe
2.BRPOP:当给定列表内没有任何元素可供弹出的时候,连接将被BRPOP命令阻塞,直到等待超时或发现可弹出元素为止。(每次只弹出一个元素,当没有元素的时候处于阻塞,当弹出一个元素之后就会解除阻塞)