Redis中的发布和订阅功能允许服务器向指定的频道发送消息,以及客户端可以订阅感兴趣的频道来接收消息。发布和订阅功能的实现主要由如下几个命令实现:

    • PUBLISH:用于服务器向指定的频道发送消息,格式为:PUBLISH CHANNEL MESSAGE
    • SUBSCRIBE:用于客户端订阅服务器指定具体名字的频道,格式为:SUBCRIBE CHANNEL_NAME
    • PSUBCRIBE:用于客户端订阅服务器指定匹配模式的频道,格式为:SUBCRIBE CHANNEL_PATTERN

    知道了发布订阅功能的相关命令后,下面来看一下各个命令底层的实现原理。总体来说,发布和订阅功能相关的状态都保存在RedisServer中的pubsub_channels和pattern两个字段中:

    1. struct redisServer{
    2. // ...
    3. dict *pubsub_channels; // 保存所有订阅的频道关系
    4. list *pattern; // 保存所有订阅的频道的匹配模式
    5. // ....
    6. }

    其中dict类型的pubsub_channels保存所有订阅的频道关系,key就是对应的频道名,value就是所有订阅该频道的客户端。由于订阅频道的客户端可能有多个,这里采用了链表的形式进行保存。如下所示:
    image.png
    当客户端订阅某个频道时,如果pubsub_channels中已经有了该频道名,说明该频道已经有订阅者,将其直接添加到订阅者链表的尾部即可。如果pubsub_channels并没有指定的频道名,需要先将频道添加到pubsub_channels中,然后该客户端作为链表的头节点存在。
    image.png

    退订频道和订阅频道的动作相反,如果某个客户端想要退订某个频道,服务器会遍历pubsub_channels,找到对应频道的订阅者链表,将其从链表中删除。此外,如果该客户端是这个频道的唯一订阅者,删除订阅者后,还需要将该频道从pubsub_channels中删除。
    image.png

    模式的订阅和退订实现与频道的订阅和退订实现类似,RedisServer的pubsub_patterns维护一个由pubsubPattern项组成的链表,其中pubsubPattern又包含client和pattern两个属性,用于表示具体的客户端和订阅的模式。

    1. struct pubsubPattern{
    2. redisClient client;
    3. robj *pattern;
    4. }

    当客户端订阅某个模式时,服务器会创建一个pubsubPattern结构,将其pattern属性设置为被订阅模式,最后将其添加到链表的尾部。如果客户端退订某个模式时,服务器会在链表中找到对应的pubsubPattern结构,并删除这些结构。
    image.png

    当服务器对某个频道发布消息时,需要执行如下的两个动作:

    • 在pubsub_channels中找到具体的频道,向订阅者列表中的每个客户端发送消息
    • 遍历pubsub_patterns找到匹配的模式,然后向对应的客户端发送消息

    另外,Redis提供了PUBSUB命令来用于服务器获取订阅信息,其中又分为如下的三个子命令:

    • PUBSUB CHANNELS:获取服务器当前被订阅的频道,格式为:PUBSUB CHANNELS [pattern]
    • PUBSUB NUMSUB:返回指定频道的订阅者数量,格式为:PUBSUB NUMSUB [channel-1 channel-2 … channel-n]
    • PUBSUB NUNPAT:获取服务器当前被订阅模式的数量,格式为:PUBSUB NUMPAT

    下面通过简单的代码看一下发布订阅功能的实现,初始时服务器中没有任何频道:

    1. 127.0.0.1:6379> pubsub channels
    2. (empty array)

    然后使用SUBSCRIBE向服务器中添加两个频道:

    1. 127.0.0.1:6379> SUBSCRIBE "news_it" "news_sport"
    2. Reading messages... (press Ctrl-C to quit)
    3. 1) "subscribe"
    4. 2) "news_it"
    5. 3) (integer) 1
    6. 1) "subscribe"
    7. 2) "news_sport"
    8. 3) (integer) 2

    此时,服务器中就存在了new_it和news_sport两个频道,并且客户端一直准备接受消息。使用PUBSUB CHANNELS查看可以看到频道已经存在。

    1. 127.0.0.1:6379> pubsub channels
    2. 1) "news_it"
    3. 2) "news_sport"

    接着向new_it发送一条消息:

    1. 127.0.0.1:6379> PUBLISH "new_it" "redis good"
    2. (integer) 1

    在订阅了频道的客户端中就可以看到服务器发布的消息:

    1. 127.0.0.1:6379> SUBSCRIBE "new_it"
    2. Reading messages... (press Ctrl-C to quit)
    3. 1) "subscribe"
    4. 2) "new_it"
    5. 3) (integer) 1
    6. 1) "message"
    7. 2) "new_it"
    8. 3) "redis good"

    如果订阅了某个模式:

    1. 127.0.0.1:6379> PSUBSCRIBE "news_*"
    2. Reading messages... (press Ctrl-C to quit)
    3. 1) "psubscribe"
    4. 2) "news_*"
    5. 3) (integer) 1
    6. 1) "pmessage"
    7. 2) "news_*"

    当向newsit或news_sport发布消息时,订阅了news*的客户端都可以收到消息。例如,分别向news_it和news_sport频道发布消息:

    1. 127.0.0.1:6379> PUBLISH "news_sport" "NBA NBA"
    2. (integer) 2

    订阅了newssport频道和news*模式的客户端都可以收到消息。

    1. 127.0.0.1:6379> SUBSCRIBE "news_it" "news_sport"
    2. Reading messages... (press Ctrl-C to quit)
    3. 1) "subscribe"
    4. 2) "news_it"
    5. 3) (integer) 1
    6. 1) "subscribe"
    7. 2) "news_sport"
    8. 3) (integer) 2
    9. 1) "message"
    10. 2) "news_it"
    11. 3) "Redis Redis"
    12. 1) "message"
    13. 2) "news_sport"
    14. 3) "NBA NBA"
    1. 127.0.0.1:6379> PSUBSCRIBE "news_*"
    2. Reading messages... (press Ctrl-C to quit)
    3. 1) "psubscribe"
    4. 2) "news_*"
    5. 3) (integer) 1
    6. 1) "pmessage"
    7. 2) "news_*"
    8. 3) "news_it"
    9. 4) "Redis Redis"
    10. 1) "pmessage"
    11. 2) "news_*"
    12. 3) "news_sport"
    13. 4) "NBA NBA"

    最后看一下PUBSUB命令的使用,如下所示:

    1. 127.0.0.1:6379> PUBSUB channels
    2. 1) "news_it"
    3. 2) "news_sport"
    4. 127.0.0.1:6379> PUBSUB NUMSUB "news_it" "news_sport"
    5. 1) "news_it"
    6. 2) (integer) 1
    7. 3) "news_sport"
    8. 4) (integer) 1
    9. 127.0.0.1:6379> PUBSUB NUMPAT
    10. (integer) 1