|
|
51CTO旗下网站
|
|
移动端

一文带你了解 Redis 的发布与订阅的底层原理

这篇文章主要是给大家介绍 Redis 的发布订阅系统,很多时候我们可能不需要独立部署相应的消息队列,只是简单的使用,而且数据量也不会太大,这种情况下,我们就可以使用 Redis 的 Pub/Sub 模型。

作者:子悠来源:Java极客技术|2020-01-02 09:57

 

01、前言

发布订阅系统在我们日常的工作中经常会使用到,这种场景大部分情况我们都是使用消息队列的,常用的消息队列有 Kafka,RocketMQ,RabbitMQ,每一种消息队列都有其特性。这篇文章主要是给大家介绍 Redis 的发布订阅系统,很多时候我们可能不需要独立部署相应的消息队列,只是简单的使用,而且数据量也不会太大,这种情况下,我们就可以使用 Redis 的 Pub/Sub 模型。

02、使用方式

2.1 发布与订阅

Redis 的发布订阅功能主要由 PUBLISH,SUBSCRIBE,PSUBSCRIBE 命令组成,一个或者多个客户端订阅某个或者多个频道,当其他客户端向该频道发送消息的时候,订阅了该频道的客户端都会收到对应的消息。

上图中有四个客户端,Client 02,Client 03,Client 04 订阅了同一个 Sport 频道(Channel),这时当 Client 01 向 Sport Channel 发送消息 “basketball” 的时候,02-04 这三个客户端都同时收到了这条消息。

整个过程的执行命令如下:

首先开四个 Redis 的客户端,然后在 Client 02,Client 03,Client 04 中输入subscribe sport 命令,表示订阅 sport 这个频道

然后在 Client 01 的客户端中输入publish sport basketball 表示向 sport 频道发送消息 "basketball"

这个时候我们在去看下 Client 02-04 的客户端,可以看到已经收到了消息了,每个订阅了这个频道的客户端都是一样的。

这里 Client 02-Client 04 三个客户端订阅了 Sport 频道,我们叫做订阅者(subscriber),Client 01 发布消息,我们叫做发布者(publisher),发送的消息就是 message。

2.2、模式订阅

前面我们看到的是一个客户端订阅了一个 Channel,事实上单个客户端也可以同时订阅多个 Channel,采用模式匹配的方式,一个客户端可以同时订阅多个 Channel。

如上图 Client 05 通过命令subscribe run 订阅了 run 频道,Client 06 通过命令psubscribe run* 订阅了 run* 匹配的频道。当 Client 07 向 run 频道发送消息 666 的时候,05 和 06 两个客户端都收到消息了;接下来 Client 07 向 run1 和 run_sport 两个频道发送消息的时候,Client 06 依旧可以收到消息,而 Client 05 就收不到了消息了。

Client 05 订阅run 频道和接收到消息:

Client 06 订阅run* 频道和接收到消息:

Client 07 向多个频道发送消息:

通过上面的案例,我们学会了一个客户端可以订阅单个或者多个频道,分别通过subscribe,psubscribe 命令,客户端可以通过 publish 发送相应的消息。

在命令行中我们可以用 Ctrl + C 来取消相关订阅,对应的命令时 unsubscribe channelName。

03、Pub/Sub 底层存储结构

3.1、订阅 Channel

在 Redis 的底层结构中,客户端和频道的订阅关系是通过一个字典加链表的结构保存的,形式如下:

在 Redis 的底层结构中,Redis 服务器结构体中定义了一个 pubsub_channels 字典

struct redisServer { //用于保存所有频道的订阅关系 dict *pubsub_channels;}

在这个字典中,key 代表的是频道名称,value 是一个链表,这个链表里面存放的是所有订阅这个频道的客户端。

所以当有客户端执行订阅频道的动作的时候,服务器就会将客户端与被订阅的频道在 pubsub_channels 字典中进行关联。

这个时候有两种情况:

该渠道是首次被订阅:首次被订阅说明在字典中并不存在该渠道的信息,那么程序首先要创建一个对应的 key,并且要赋值一个空链表,然后将对应的客户端加入到链表中。此时链表只有一个元素。

该渠道已经被其他客户端订阅过:这个时候就直接将对应的客户端信息添加到链表的末尾就好了。

比如,如果有一个新的客户端 Client 08 要订阅 run 渠道,那么上图就会变成

如果 Client 08 要订阅一个新的渠道 new_sport ,那么就会变成

整个订阅的过程可以采用下面伪代码来实现

  1. Map<String, List<Object>> pubsub_channels = new HashMap<>(); 
  2.     public void subscribe(String[] subscribeList, Object client) { 
  3.         //遍历所有订阅的 channel,检查是否在 pubsub_channels 中,不在则创建新的 key 和空链表 
  4.         for (int i = 0; i < subscribeList.length; i++) { 
  5.             if (!pubsub_channels.containsKey(subscribeList[i])) { 
  6.                 pubsub_channels.put(subscribeList[i], new ArrayList<>()); 
  7.             } 
  8.             pubsub_channels.get(subscribeList[i]).add(client); 
  9.         } 
  10.     } 

3.2 取消订阅

上面介绍的是单个 Channel 的订阅,相反的如果一个客户端要取消订阅相关 Channel,则无非是找到对应的 Channel 的链表,从中删除对应的客户端,如果该客户端已经是最后一个了,则将对应 Channel 也删除。

  1. public void unSubscribe(String[] subscribeList, Object client) { 
  2.         //遍历所有订阅的 channel,依次删除 
  3.         for (int i = 0; i < subscribeList.length; i++) { 
  4.             pubsub_channels.get(subscribeList[i]).remove(client); 
  5.             //如果长度为 0 则清楚 channel 
  6.             if (pubsub_channels.get(subscribeList[i]).size() == 0) { 
  7.                 remove(subscribeList[i]); 
  8.             } 
  9.         } 
  10.     } 

04、模式订阅结构

模式渠道的订阅与单个渠道的订阅类似,不过服务器是将所有模式的订阅关系都保存在服务器状态的pubsub_patterns 属性里面。

  1. struct redisServer{ 
  2.     //保存所有模式订阅关系 
  3.     list *pubsub_patterns; 

与订阅单个 Channel 不同的是,pubsub_patterns 属性是一个链表,不是字典。节点的结构如下:

  1. struct pubsubPattern{ 
  2.     //订阅模式的客户端 
  3.     redisClient *client; 
  4.     //被订阅的模式 
  5.     robj *pattern; 
  6. } pubsubPattern; 

其实 client 属性是用来存放对应客户端信息,pattern 是用来存放客户端对应的匹配模式。

所以对应上面的 Client-06 模式匹配的结构存储如下

在pubsub_patterns链表中有一个节点,对应的客户端是 Client-06,对应的匹配模式是run*。

4.1、订阅模式

当某个客户端通过命令psubscribe 订阅对应模式的 Channel 时候,服务器会创建一个节点,并将 Client 属性设置为对应的客户端,pattern 属性设置成对应的模式规则,然后添加到链表尾部。

对应的伪代码如下:

  1. List<PubSubPattern> pubsub_patterns = new ArrayList<>(); 
  2.     public void psubscribe(String[] subscribeList, Object client) { 
  3.         //遍历所有订阅的 channel,创建节点 
  4.         for (int i = 0; i < subscribeList.length; i++) { 
  5.             PubSubPattern pubSubPattern = new PubSubPattern(); 
  6.             pubSubPattern.client = client; 
  7.             pubSubPattern.pattern = subscribeList[i]; 
  8.             pubsub_patterns.add(pubSubPattern); 
  9.         } 
  10.     } 
  1. 创建新节点;
  2. 给节点的属性赋值;
  3. 将节点添加到链表的尾部;

4.2、退订模式

退订模式的命令是punsubscribe,客户端使用这个命令来退订一个或者多个模式 Channel。服务器接收到该命令后,会遍历pubsub_patterns链表,将匹配到的 client 和 pattern 属性的节点给删掉。这里需要判断 client 属性和 pattern 属性都合法的时候再进行删除。

伪代码如下:

  1. public void punsubscribe(String[] subscribeList, Object client) { 
  2.         //遍历所有订阅的 channel 相同 client 和 pattern 属性的节点会删除 
  3.         for (int i = 0; i < subscribeList.length; i++) { 
  4.             for (int j = 0; j < pubsub_patterns.size(); j++) { 
  5.                 if (pubsub_patterns.get(j).client == client 
  6.                 && pubsub_patterns.get(j).pattern == subscribeList[i]) { 
  7.                     remove(pubsub_patterns); 
  8.                 } 
  9.             } 
  10.         } 
  11.     } 

遍历所有的节点,当匹配到相同 client 属性和 pattern 属性的时候就进行节点删除。

05、发布消息

发布消息比较好容易理解,当一个客户端执行了publish channelName message 命令的时候,服务器会从pubsub_channels和pubsub_patterns 两个结构中找到符合channelName 的所有 Channel,进行消息的发送。在 pubsub_channels 中只要找到对应的 Channel 的 key 然后向对应的 value 链表中的客户端发送消息就好。

06、总结

这篇文章主要给大家介绍了一下 Redis 的发布/订阅的使用方式和底层的存储结构以及部分伪代码的实现,希望对大家有帮助。

【编辑推荐】

  1. 原来Redis的五种数据类型数底层结构是这样的
  2. Nutanix在中国市场发布多云合作伙伴计划
  3. 掌握这些Redis技巧,让你掌控住千亿数据量
  4. 分布式系统之Redis主从架构
  5. Redis集群架构了解一下?一致性Hash了解吗?
【责任编辑:武晓燕 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

订阅专栏+更多

Python应用场景实战手册

Python应用场景实战手册

Python应用场景实战手册
共3章 | KaliArch

7人订阅学习

一步到位玩儿透Ansible

一步到位玩儿透Ansible

Ansible
共17章 | 骏马金龙1

101人订阅学习

云架构师修炼手册

云架构师修炼手册

云架构师的必备技能
共3章 | Allen在路上

30人订阅学习

读 书 +更多

数据挖掘:概念与技术

本书第1版曾被KDnuggets的读者评选为最受欢迎的数据挖掘专著,是一本可读性极佳的教材。它从数据库角度全面系统地介绍了数据挖掘的基本概念...

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊

51CTO服务号

51CTO官微