Redis的订阅和发布
发布与订阅模型,放在每一位开发者身上,都是耳熟能详的一对词语。与设计模式里面的--观察者模式非常相似。在微服务中,把消息中介分离出来成为独立服务,无论是跨服务通信开发和运维角度,redis的发布订阅模式似乎是很好的选择。
我们先从原理和使用实例出发来理解一下redis的这个发布与订阅是怎么运行的,再结合开发场景说说它的优势和劣势。
首先我们理解一点,发布者可以做1:N的广播,反过来说,多个订阅者可以同时订阅一个发布者的消息。
结构图大致如此:
背后原理:
一、精确订阅
Redis服务器中维护着一个pubsub_channels字典(可以类似理解为map或者键值对),所有的频道和订阅关系都存储在这里。字典的键为频道的名称,而值为订阅频道的客户端连接链表。
我们可以将操作行为分为两类,订阅者行为和发布者行为。
订阅者,订阅频道过程:
当有新的客户端订阅某个频道时,会发生两种情况中的一种:
1)如果频道已经存在,则新的客户端会添加到pubsub_channels对应频道的链表末尾
2)如果频道原本不存在,则会为频道创建一个键,该客户端成为链表的第一个元素
订阅者,退订频道过程:
当一个客户端退订一个频道的时候,pubsub_channels对应键的链表会删除该客户端
发布者,发送信息过程:
服务器会遍历pubsub_channels中对应键的链表,向每一个客户端发送信息
这也是为什么发布者能实现1:N广播的原因了。
二、模糊订阅
服务器维护着一个pubsub_patterns链表,链表的pattern属性记录了被订阅的模式,而client属性记录了订阅模式的客户端信息
订阅者,订阅模式过程:
当有新的客户端订阅某个模式的时,会进行如下步骤:
1)创建一个链表节点,pattern属性记录订阅的模式,client记录订阅模式的客户端
2)将这个链表节点添加到pubsub_patterns链表中
订阅者,退订模式过程:
当一个客户端退订某一个模式的时候,服务器遍历pubsub_patterns找到对应的pattern,再往下找到对应该client客户端的节点,将该节点删除
发布者,发送信息过程:
服务器遍历pubsub_channels,查找与channels频道相匹配的模式麻将消息发送给订阅了这些模式的客户端。
Redis订阅系统的优势:
1. 当一个客户端向频道发送一个信息,订阅了同一个频道/模式的多个客户端可以同时接收到信息,类似广播的机制。
2. 便于Sentinel哨兵与服务器间的通信并进行监控
实例操作:
1. 订阅频道
命令格式:
SUSCRIBE channel1 [channel2] [channel3]
例如(同时订阅两个频道):
redis> subscribe local_system remote_system # 订阅者同时订阅两个频道
结果 Reading messages... (press Ctrl-C to quit) 1) "subscribe" # 返回值的类型:显示订阅成功 2) "local_system" # 订阅的频道名字 3)"remote _system " 4) (integer) 2 # 目前已订阅的频道数量
发布者发布信息:
redis> publish local_system hello
(integer) 1 # 消息发送成功
注意:客户端一点与某个频道建立连接就不能进行其他操作,一旦客户端退出连接,订阅的频道自动会断开。
2. 订阅模式
PSUBSCRIBE pattern1 [pattern2] [pattern3] pattern可以用符号*进行模糊匹配,例如: remo* #匹配以remo开头的模式 *system #匹配以system结尾的模式
操作案例:
订阅者,模糊订阅频道
发布者,发布信息:
订阅者,获取到信息:
3. 取消订阅:
命令: 1)取消订阅频道 UNSUBSCRIBE channel1 [channel2] 2)取消订阅模式 PUNSUBSCRIBE [pattern [pattern …]]
4. 发布消息
命令:
PUBLISH channel message
向一个频道发送信息,所有订阅这个频道的客户端都会接收到信息
其他辅助命令
查看已有的频道
命令: PUBSUB subcommand [argument [argument ...]] subcommand子命令: 1)PUBSUB CHANNELS [pattern] 列出当前的活跃频道 如果给定pattern参数,则会返回服务器当前被订阅的频道中与pattern模式相匹配的频道
2)PUBSUB NUMSUB [channel-1 … channel-N]
3)PUBSUB NUMPAT
返回订阅模式的数量
至此,我们大致理解了Redis的发布订阅机制是如何运行的。那么实际有什么应用场景呢?
1)点赞功能,给多个客户端更新最新的点赞数量。
由于HTTP无状态的特性,我们可以用websocket来解决通信双方保活的问题,服务端一旦接收到发布者更新的点赞数量,就可以通过websocket发送给客户端。
由此延伸开来,类似的更新与推送,都可以用Rredis + Websocket得到实现。要举的例子还可以很多很多。。。
2)作为消息队列
服务端内部服务之间的通信,可以通过Redis发布订阅机制实现消息广播
优劣势:
1)优势
基于内存,通信效率高。使用足够简单
2)劣势
在探讨一个中间件是否适合做消息队列的时候,核心目标就是不允许数据丢失。而消息是否会发生丢失,围绕的重点也就在于以下 3 个环节:
1. 生产者会不会丢消息?
2. 消费者会不会丢消息?
3. 消息队列中间件会不会丢消息?
问题1/3:有概率丢失。当内存撑得很大的时候,Redis是有概率会丢失数据的。
问题2:显然,一旦订阅者离线,Redis的消息就会丢失。
基于以上的分析,一个建议:
对于消息并非非常重要,丢失中间的某些消息也不影响系统正常运行和用户体验的前提下,可以使用Redis的订阅发布机制。
如果对于消息的准确率要求非常高的场景,有更好的替代方案,使用专业的消息中间件,例如RabbitMQ、Activemq、Kafka等等
题外话
Sentinel服务器与Master服务器/Slave服务器之间的订阅发布系统是Sentinl(哨兵)监控过程的一个重要环节,通过订阅发布系统达到监控服务器状态的作用。其运行原理与上面的客户端服务器之间的订阅机制无太大区别,都是基于网络连接的数据传输。Sentinel之间的通信也是通过Sentinel与服务器间的这个订阅发布机制实现的,一个Sentinel通过服务器的频道发送信息,其他Sentinel就会接收到。