Redis队列(三):消息的发布与订阅的C源码实现分析

本文深入剖析Redis的消息订阅与发布功能,包括精确订阅与模式订阅的实现原理,以及消息发布的过程。通过源代码分析,详细介绍了Redis如何在客户端和服务端维护订阅关系,以及消息是如何实时传输给订阅者的。

一、概述

  • 在之前的文章分析过,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的所有客户端,不会进行消息存储。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值