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

01、前言

发布订阅系统在我们日常的工作中经常会使用到,这种场景大部分情况我们都是使用消息队列的,常用的消息队列有 Kafka,RocketMQ,RabbitMQ,每一种消息队列都有其特性,关于 Kafka 的使用和源码分析,公号前面有相关的文章,大家可以前往回顾一下,另外两款消息队列大家有需要可以自行研究,后续我们会出相应的介绍文章。这篇文章主要是给大家介绍 Redis 的发布订阅系统,很多时候我们可能不需要独立部署相应的消息队列,只是简单的使用,而且数据量也不会太大,这种情况下,我们就可以使用 Redis 的 Pub/Sub 模型。

02、使用方式

2.1 发布与订阅

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

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

上图中有四个客户端,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 这个频道

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

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

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

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

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

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

2.2、模式订阅

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

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

如上图 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 频道和接收到消息:

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

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

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

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

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

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

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

03、Pub/Sub 底层存储结构

3.1、订阅 Channel

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

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

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

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

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

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

这个时候有两种情况:

  • 该渠道是首次被订阅:首次被订阅说明在字典中并不存在该渠道的信息,那么程序首先要创建一个对应的 key,并且要赋值一个空链表,然后将对应的客户端加入到链表中。此时链表只有一个元素。
  • 该渠道已经被其他客户端订阅过:这个时候就直接将对应的客户端信息添加到链表的末尾就好了。

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

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

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

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

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

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

3.2 取消订阅

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

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

04、模式订阅结构

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

struct redisServer{//保存所有模式订阅关系list *pubsub_patterns;}

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

struct pubsubPattern{//订阅模式的客户端redisClient *client;//被订阅的模式robj *pattern;} pubsubPattern;

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

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

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

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

4.1、订阅模式

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

对应的伪代码如下:

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

4.2、退订模式

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

伪代码如下:

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

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

05、发布消息

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

06、总结

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

觉得文章不错就给小老弟点个关注吧,更多内容陆续奉上。

最后,分享一份面试宝典《Java核心知识点整理.pdf》,覆盖了JVM、锁、高并发、反射、Spring原理、微服务、Zookeeper、数据库、数据结构等等。私信回复“资料”获取免费领取方式。

发布了55 篇原创文章 · 获赞 1401 · 访问量 33万+

猜你喜欢

转载自blog.csdn.net/yelvgou9995/article/details/103748151