为什么还要发布订阅模型?
前面我们讲了Redis消息队列的使用方法,但是没有提到Redis消息队列的不足。Redis消息队列的一个很大的不足就是无法支持消息的多播机制
,正因为如此,Redis才提出了发布订阅模型!
消息多播
消息多播允许生产者值生成一次消息,由中间件负责将消息复制到多个消息队列中,每个消息队列由相应的消费组进行消费。
PubSub
为了支持消息多播,Redis不能再依赖基本的数据类型实现了,它单独的使用了一个模块来支持消息多播的功能,既PubSub
模块, 也就是通常我们所说的发布/订阅模型
发布订阅模型的缺点
离线节点彻底丢失消息
Redis的PubSub模块的生产者发送了一个消息给订阅者,Redis会直接找到订阅的消费者发送。如果一个消费者都没有,那么生产者的消息就相当石沉大海,直接被丢弃。
所以这就会造成一个很大的弊端。当有三个订阅者订阅了一个生产者,突然某个消费者节点离线了,过了一会又上线了。那么在该消费者离线期间,生成者所推送的内容,相对该掉线消费者就是彻底丢失了。
没有消息确认机制
只要消息从Redis的队列中pop出去后,就再也不跟Redis有任何关系了,业务逻辑执行错了,消息也不会回到Redis中。对比RabbitMQ就可以做到,业务执行失败,no-ack,消息会回到Rabbit队列中
无法保证消息真实到达中间件,既消息从发布者客户端发送出现后,是否有到达Redis服务器,是无法知道的。相比RabbitMQ就可以有Comfirm机制
消息不能持久化
因为PubSub的消息并非是Redis的一种数据类型,所以PubSub中数据是没有得到aof等持久化机制的支持的,既一旦节点崩溃,重新上线后,消息是会丢失的。这一点在Redis 5.0的Stream新数据结构中得到了改善
也正因为PubSub模块的这些缺点,在严谨的消息队列领域,Redis的PubSub模型上不了主舞台,只能做一下简单的消息推送功能。总之Redis的发布订阅模块并不能作为严格意义的消息中间件,只能算是一个轻量级消息队列,可以在一些无伤大雅的低要求场景上使用。
所以Redis的作者单独开启了一个叫Disque的项目,专门做多播消息队列,但是目前该项目还未成熟。但是更让人劲爆的消息是,Redis5.0新增了一个Stream数据结构,它是一个强大的支持多播的可持久化的消息队列。所以在可预见的未来,PubSub会逐渐被淘汰,Disque项目也可能不会有发行版本了。
发布/订阅模型的命令
-
publish
将信息 message 发送到指定的频道 channel,返回频道订阅者数量
publish channel message
-
subscribe
订阅一个或多个频道
subscribe channel [channel...]
-
psubscribe
订阅符合一个或多个匹配模式的所有频道,psubscribe new.* 则是订阅所有new.开头的频道(new.log,new.studnet,etc…)
psubscribe pattern [pattern …]
-
unsubscribe
退订一个或多个的指定频道
unsubscribe channel [channel...]
-
punsubscribe
退订符合一个或多个匹配模式的所有频道
punsubcribe pattern [pattern]
-
pubsub
pubsub是一个查看订阅与发布系统状态的内省命令, 它由数个不同格式的子命令组成pubsub
pubsub channels [pattern]
查询系统中符合模式的频道信息,pattern为空,则查询系统中所有存在的频道pubsub numsub [channel]
查询一个或多个频道的订阅数pubsub numpat
查询当前客户端订阅了多少频道
我们可以看到本质就是由四种命令组成,发布命令,订阅命令,退订命令和查询命令
消息结构
{
'type': 'message',
'pattern': None,
'channel': ' python.new',
'data': 'hello python ?'
}
我们可以看到Redis的发布订阅模型中,消息传递的数据结构是有四个固定字段的
-
type
表示消息的类型
如果是一个普通的消息,那么类型就是message
如果是控制消息,比如订阅命令的反馈,那么类型就是subscribe
如果是模式订阅的返回,它的类型就是psubscribe
如果是取消订阅的指令,那么就可以是unsubscribe或punsubscribe -
pattern
表示当前消息是使用那种模式订阅的,如果是通过subscribe命令订阅的,那么该字段就为空
-
channel
表示当前订阅的频道名称,其实就相当于kafka的主题,rabbitmq的routing-key
-
data
这个就很明显的,就是数据体
代码实践
Redis PubSub | Python
发布者
import redis
client = redis.StrictRedis(host='127.0.0.1', port=6379)
# 朝python.new频道发送三条消息
client.publish('python.new', 'hello python?')
client.publish('python.new', "let's learn pythone everyday!!")
client.publish('python.new', "easy python3.7")
订阅者
# 普通版本,利用sleep来阻塞
import redis
import time
client = redis.StrictRedis(host='127.0.0.1', port=6379)
p = client.pubsub()
p.subscribe('python.new')
while True:
message = p.get_message()
if not message:
time.sleep(1)
continue
print(message)
# 改良版本,利用listen()来阻塞
import redis
import time
client = redis.StrictRedis(host='127.0.0.1', port=6379)
p = client.pubsub()
p.subscribe('python.new')
for message in p.listen():
print(message)