一、概述

  • 在之前的文章分析过,Redis的消息发布与订阅支持基于频道channel的精确订阅与基于模式pattern的模糊订阅,并且是实时的消息传输,不会进行消息存储,如下从源代码来分析Redis的订阅与发布功能的实现,主要在Redis源码的pubsub.c文件定义。

二、消息订阅

1. 客户端和服务端的订阅存储结构
  • 消息订阅主要从客户端client和服务端server两个角度来分析。首先在客户端client和服务端server都会维护一个字典类型dict的pubsub_channels来维护频道channel的订阅关系,维护一个列表类型的pubsub_patterns来维护模式pattern的订阅关系,如下redisServer为服务端数据结构,client为客户端数据结构:
// 服务端数据结构
struct redisServer {
    
    // 省略其他代码
    
    // 服务端维护频道channel和订阅了这个channel的客户端列表的映射
    // 使用字典结构维护
    dict *pubsub_channels;  /* Map channels to list of subscribed clients */
    
    // 服务端维护订阅了某个模式pattern的某个客户端的列表
    // 使用列表结构维护
    list *pubsub_patterns;  /* A list of pubsub_patterns */
    
    // 省略其他代码
}

// 客户端数据结构
typedef struct client {
    // 省略其他代码
    
    // 客户端所订阅的频道channel
    // 使用字典结构维护
    dict *pubsub_channels;  /* channels a client is interested in (SUBSCRIBE) */
    
    // 客户端所订阅的模式pattern,
    // 使用列表结构维护
    list *pubsub_patterns;  /* patterns a client is interested in (SUBSCRIBE) */
    
    // 省略其他代码
} client;
2. 频道channel的订阅数据结构
  • 服务端redisServer维护的频道channel订阅字典pubsub_channels和客户端client维护的频道channel的订阅字典pubsub_channels所存储的数据的方式不一样:
  1. 服务端存储的是channel到所有订阅了这个频道的客户端,即channel -> clients的映射,即字典的key为频道channel,值为客户端列表clients;
  2. 客户端存储的是该客户端所订阅过的频道channel,即channel -> NULL,字典的key为频道channel,而值为NULL。
3. 模式pattern的订阅数据结构
  • 模式pattern的订阅在服务端和客户端都是使用列表类型的pubsub_patterns来维护,内部存在的模式订阅数据如下:包含客户端的引用client和模式pattern。
typedef struct pubsubPattern {
    // 客户端引用
    client *client;
    // 该
    robj *pattern;
} pubsubPattern;
4. 订阅实现
频道channel订阅的源码实现
  • Redis服务端redisServer接收客户端订阅某个频道的方法实现如下:在服务端redisServer和客户端client的pubsub_channels都需要添加这个映射关系,核心方法为其中dictAdd函数的参数列表为(dict,key,value),所以从以下代码可知服务端value是客户端引用集合clients,客户端的value为NULL。
int pubsubSubscribeChannel(client *c, robj *channel) {
    dictEntry *de;
    list *clients = NULL;
    int retval = 0;

    // 添加到该客户端自身引用client的pubsub_channels中
    // 其中dictAdd函数的参数列表为(dict,key,value),
    // 所以对于客户端pubsub_channels字典来说,key的值为channel,value为NULL。
    // 即如果客户端要取消订阅某个channel,则只需要删除对应的key即可,时间复杂度为O(1)。
    if (dictAdd(c->pubsub_channels,channel,NULL) == DICT_OK) {
        retval = 1;
        incrRefCount(channel);
        
        de = dictFind(server.pubsub_channels,channel);
        if (de == NULL) {
            clients = listCreate();
            // 添加该客户端client到Redis服务端的pubsub_channels中
            // 其中dictAdd函数的参数列表为(dict,key,value),
            // 所以对于服务端的pubsub_channels字典来说,key为channel,value为订阅了这个channel的客户端列表clients
            dictAdd(server.pubsub_channels,channel,clients);
            incrRefCount(channel);
        } else {
            clients = dictGetVal(de);
        }
        // 将当前客户端c添加到记录发起过订阅的客户端列表clients
        listAddNodeTail(clients,c);
    }
    // 通知客户端订阅完成
    addReply(c,shared.mbulkhdr[3]);
    addReply(c,shared.subscribebulk);
    addReplyBulk(c,channel);
    addReplyLongLong(c,clientSubscriptionsCount(c));
    return retval;
}
模式pattern订阅的源码实现
  • 源码实现如下:创建模式pattern的订阅pubsubPattern的对象实例pat,pubsubPattern包含客户端引用client和模式字符串pattern,然后将pat对象分别添加到服务端的pubsub_patterns列表和客户端的pubsub_patterns列表。
// 客户端订阅某个模式,如item.*则是订阅所有以item.开头的channels
int pubsubSubscribePattern(client *c, robj *pattern) {
    int retval = 0;
    
    if (listSearchKey(c->pubsub_patterns,pattern) == NULL) {
        retval = 1;
        pubsubPattern *pat;
        // 在客户端引用c的pubsub_patterns列表添加这个模式pattern
        // client的pubsub_patterns为列表list类型,添加该模式pattern字符串到客户端的该列表中
        listAddNodeTail(c->pubsub_patterns,pattern);
        incrRefCount(pattern);
        // 所订阅的模式和当前当前客户端的引用c
        pat = zmalloc(sizeof(*pat));
        pat->pattern = getDecodedObject(pattern);
        pat->client = c;
        // 添加模式pattern和客户端的订阅映射pat到服务端的pubsub_patterns列表中
        listAddNodeTail(server.pubsub_patterns,pat);
    }
    // 通知客户端订阅完成
    addReply(c,shared.mbulkhdr[3]);
    addReply(c,shared.psubscribebulk);
    addReplyBulk(c,pattern);
    addReplyLongLong(c,clientSubscriptionsCount(c));
    return retval;
}

三、消息发布

  • 消息发布源码实现如下:方法参数为channel和message,即将消息message发往频道channel。在内部实现当中:
  1. 首先从服务端的pubsub_channels字典中查找订阅了这个频道channel的所有客户端,然后发送该消息message给这些客户端;
  2. 然后从服务端的pubsub_patterns列表中查找是否存在匹配这个channel的模式pattern,如果存在则将消息发送给对应的客户端client。
// 发布消息到某个频道channel
int pubsubPublishMessage(robj *channel, robj *message) {
    int receivers = 0;
    dictEntry *de;
    listNode *ln;
    listIter li;

    // 从服务端查找订阅了这个频道channel的所有客户端
    de = dictFind(server.pubsub_channels,channel);
    if (de) {
        list *list = dictGetVal(de);
        listNode *ln;
        listIter li;

        listRewind(list,&li);
        // 遍历所有订阅了这个频道channel的客户端列表
        while ((ln = listNext(&li)) != NULL) {
            client *c = ln->value;
            // 发布消息给订阅了这个频道channel的客户端
            addReply(c,shared.mbulkhdr[3]);
            addReply(c,shared.messagebulk);
            addReplyBulk(c,channel);
            addReplyBulk(c,message);
            receivers++;
        }
    }
    
    // 查找服务端的模式pattern列表,看当前消息对应的channel是否与某个模式匹配,
    // 有则发送该消息给订阅了这个模式的所有客户端
    if (listLength(server.pubsub_patterns)) {
        listRewind(server.pubsub_patterns,&li);
        channel = getDecodedObject(channel);
        while ((ln = listNext(&li)) != NULL) {
            pubsubPattern *pat = ln->value;

            if (stringmatchlen((char*)pat->pattern->ptr,
                                sdslen(pat->pattern->ptr),
                                (char*)channel->ptr,
                                sdslen(channel->ptr),0)) {
                // 发布消息给订阅了这个频道channel的客户端
                addReply(pat->client,shared.mbulkhdr[4]);
                addReply(pat->client,shared.pmessagebulk);
                addReplyBulk(pat->client,pat->pattern);
                addReplyBulk(pat->client,channel);
                addReplyBulk(pat->client,message);
                receivers++;
            }
        }
        decrRefCount(channel);
    }
    return receivers;
}
  • 所以Redis的消息发布订阅其实就是通过方法调用指定消息内容message和该消息所属的频道channel,将该消息发送给所有订阅了这个channel的所有客户端,不会进行消息存储。