一、Redis 中的发布/订阅功能
发布/ 订阅系统 是 Web 系统中比较常用的一个功能。简单点说就是 发布者发布消息,订阅者接受消息,这有点类似于我们的报纸/ 杂志社之类的: (借用前边的一张图)
- 图片引用自:「消息队列」看过来! - https://www.wmyskxz.com/2019/07/16/xiao-xi-dui-lie-kan-guo-lai/
从我们 前面(下方相关阅读) 学习的知识来看,我们虽然可以使用一个 list
列表结构结合 lpush
和 rpop
来实现消息队列的功能,但是似乎很难实现实现 消息多播 的功能:
为了支持消息多播,Redis 不能再依赖于那 5 种基础的数据结构了,它单独使用了一个模块来支持消息多播,这个模块就是 PubSub,也就是 PublisherSubscriber (发布者/ 订阅者模式)。
PubSub 简介
我们从 上面的图 中可以看到,基于 list
结构的消息队列,是一种 Publisher
与 Consumer
点对点的强关联关系,Redis 为了消除这样的强关联,引入了另一种概念:频道 (channel):
当 Publisher
往 channel
中发布消息时,关注了指定 channel
的 Consumer
就能够同时受到消息。但这里的 问题 是,消费者订阅一个频道是必须 明确指定频道名称 的,这意味着,如果我们想要 订阅多个 频道,那么就必须 显式地关注多个 名称。
为了简化订阅的繁琐操作,Redis 提供了 模式订阅 的功能 Pattern Subscribe,这样就可以 一次性关注多个频道 了,即使生产者新增了同模式的频道,消费者也可以立即受到消息:
例如上图中,所有 位于图片下方的 Consumer
都能够受到消息。
Publisher
往 wmyskxz.chat
这个 channel
中发送了一条消息,不仅仅关注了这个频道的 Consumer 1
和 Consumer 2
能够受到消息,图片中的两个 channel
都和模式 wmyskxz.*
匹配,所以 Redis 此时会同样发送消息给订阅了 wmyskxz.*
这个模式的 Consumer 3
和关注了在这个模式下的另一个频道 wmyskxz.log
下的 Consumer 4
和 Consumer 5
。
另一方面,如果接收消息的频道是 wmyskxz.chat
,那么 Consumer 3
也会受到消息。
快速体验
在 Redis 中,PubSub 模块的使用非常简单,常用的命令也就下面这么几条:
# 订阅频道:
SUBSCRIBE channel [channel ....] # 订阅给定的一个或多个频道的信息
PSUBSCRIBE pattern [pattern ....] # 订阅一个或多个符合给定模式的频道
# 发布频道:
PUBLISH channel message # 将消息发送到指定的频道
# 退订频道:
UNSUBSCRIBE [channel [channel ....]] # 退订指定的频道
PUNSUBSCRIBE [pattern [pattern ....]] #退订所有给定模式的频道
我们可以在本地快速地来体验一下 PubSub:
具体步骤如下:
- 开启本地 Redis 服务,新建两个控制台窗口;
- 在其中一个窗口输入
SUBSCRIBE wmyskxz.chat
关注wmyskxz.chat
频道,让这个窗口成为 消费者。 - 在另一个窗口输入
PUBLISH wmyskxz.chat 'message'
往这个频道发送消息,这个时候就会看到 另一个窗口实时地出现 了发送的测试消息。
实现原理
可以看到,我们通过很简单的两条命令,几乎就可以简单使用这样的一个 发布/ 订阅系统 了,但是具体是怎么样实现的呢?
每个 Redis 服务器进程维持着一个标识服务器状态 的 redis.h/redisServer
结构,其中就 保存着有订阅的频道 以及 订阅模式 的信息:
struct redisServer {
// ...
dict *pubsub_channels; // 订阅频道
list *pubsub_patterns; // 订阅模式
// ...
};
订阅频道原理
当客户端订阅某一个频道之后,Redis 就会往 pubsub_channels
这个字典中新添加一条数据,实际上这个 dict
字典维护的是一张链表,比如,下图展示的 pubsub_channels
示例中,client 1
、client 2
就订阅了 channel 1
,而其他频道也分别被其他客户端订阅:
SUBSCRIBE 命令
SUBSCRIBE
命令的行为可以用下列的伪代码表示:
def SUBSCRIBE(client, channels):
# 遍历所有输入频道
for channel in channels:
# 将客户端添加到链表的末尾
redisServer.pubsub_channels[channel].append(client)
通过 pubsub_channels
字典,程序只要检查某个频道是否为字典的键,就可以知道该频道是否正在被客户端订阅;只要取出某个键的值,就可以得到所有订阅该频道的客户端的信息。
PUBLISH 命令
了解 SUBSCRIBE
,那么 PUBLISH
命令的实现也变得十分简单了,只需要通过上述字典定位到具体的客户端,再把消息发送给它们就好了:(伪代码实现如下)
def PUBLISH(channel, message):
# 遍历所有订阅频道 channel 的客户端
for client in server.pubsub_channels[channel]:
# 将信息发送给它们
send_message(client, message)
UNSUBSCRIBE 命令
使用 UNSUBSCRIBE
命令可以退订指定的频道,这个命令执行的是订阅的反操作:它从 pubsub_channels
字典的给定频道(键)中,删除关于当前客户端的信息,这样被退订频道的信息就不会再发送给这个客户端。
订阅模式原理
正如我们上面说到了,当发送一条消息到 wmyskxz.chat
这个频道时,Redis 不仅仅会发送到当前的频道,还会发送到匹配于当前模式的所有频道,实际上,pubsub_patterns
背后还维护了一个 redis.h/pubsubPattern
结构:
typedef struct pubsubPattern {
redisClient *client; // 订阅模式的客户端
robj *pattern; // 订阅的模式