zoukankan      html  css  js  c++  java
  • redis学习笔记(七): pubsub

    redis简单地实现了订阅发布功能。

    pubsub涉及到的结构主要是下面两个:

    typedef struct redisClient {
        ...
        dict *pubsub_channels;  //该client订阅的channels,以channel为key用dict的方式组织
        list *pubsub_patterns;  //该client订阅的pattern,以list的方式组织
        ...
    } redisClient;
    
    struct redisServer {
        ...
        dict *pubsub_channels;      //redis server进程中维护的channel dict,它以channel为key,订阅channel的client list为value
        list *pubsub_patterns;      //redis server进程中维护的pattern list
        int notify_keyspace_events; 
        ...
    };

    没搞懂的是在redisClient中,为什么channel和pattern一个用dict一个用list?

    对应的command:

    struct redisCommand redisCommandTable[] = {
        ...
        {"subscribe",    subscribeCommand,    -2,"rpslt",0,NULL,0,0,0,0,0},    //channel订阅命令
        {"unsubscribe",  unsubscribeCommand,  -1,"rpslt",0,NULL,0,0,0,0,0},    //channel退订命令
        {"psubscribe",   psubscribeCommand,   -2,"rpslt",0,NULL,0,0,0,0,0},    //pattern订阅命令
        {"punsubscribe", punsubscribeCommand, -1,"rpslt",0,NULL,0,0,0,0,0},    //pattern退订命令
        {"publish",      publishCommand,       3,"pltrF",0,NULL,0,0,0,0,0},    //消息发布命令
        {"pubsub",       pubsubCommand,       -2,"pltrR",0,NULL,0,0,0,0,0},    //pubsub命令,用于输出channel相关的统计信息
        ...
    }

    pattern的匹配,里面调用的equalStringObjects就是redis实现的正则匹配:

    int listMatchPubsubPattern(void *a, void *b) {
        pubsubPattern *pa = a, *pb = b;
    
        return (pa->client == pb->client) &&
               (equalStringObjects(pa->pattern,pb->pattern));
    }

    订阅某个channel的核心操作

    int pubsubSubscribeChannel(redisClient *c, robj *channel) {
        dictEntry *de;
        list *clients = NULL;
        int retval = 0;
    
        /* Add the channel to the client -> channels hash table */
        /* 以channel为key加到c->pubsub_channels当中 */
        if (dictAdd(c->pubsub_channels,channel,NULL) == DICT_OK) {
            retval = 1;
            incrRefCount(channel);
            /* Add the client to the channel -> list of clients hash table */
            /* server.pubsub_channels记录了所有被订阅的channel以及订阅特定channel的clients, 这里把c加到该channel对应的列表当中 */
            de = dictFind(server.pubsub_channels,channel);
            if (de == NULL) {
                clients = listCreate();
                dictAdd(server.pubsub_channels,channel,clients);
                incrRefCount(channel);
            } else {
                clients = dictGetVal(de);
            }
            listAddNodeTail(clients,c);
        }
        /* Notify the client */
        /* 给客户端生成答复 */
        addReply(c,shared.mbulkhdr[3]);
        addReply(c,shared.subscribebulk);
        addReplyBulk(c,channel);
        addReplyLongLong(c,clientSubscriptionsCount(c));
        return retval;
    }

    退订某个channel的核心操作

    int pubsubUnsubscribeChannel(redisClient *c, robj *channel, int notify) {
        dictEntry *de;
        list *clients;
        listNode *ln;
        int retval = 0;
    
        /* Remove the channel from the client -> channels hash table */
        /* 为什么需要保护起来?
         * 考虑到redis本身是单进程单线程的,所以这里不是为了防止在别的地方被free掉     
         */
        incrRefCount(channel); /* channel may be just a pointer to the same object
                                we have in the hash tables. Protect it... */
        /* 从该client的订阅dict中移除该channel */
        if (dictDelete(c->pubsub_channels,channel) == DICT_OK) {
            retval = 1;
            /* Remove the client from the channel -> clients list hash table */
            /* 从server.pubsub_channels中该channel维护的client列表上删除该client */
            de = dictFind(server.pubsub_channels,channel);
            redisAssertWithInfo(c,NULL,de != NULL);
            clients = dictGetVal(de);
            ln = listSearchKey(clients,c);
            redisAssertWithInfo(c,NULL,ln != NULL);
            listDelNode(clients,ln);
            /* 如果该channel上没有其它订阅者,则从server.pubsub_channels上删除该channel */
            if (listLength(clients) == 0) {
                /* Free the list and associated hash entry at all if this was
                 * the latest client, so that it will be possible to abuse
                 * Redis PUBSUB creating millions of channels. */
                dictDelete(server.pubsub_channels,channel);
            }
        }
        /* Notify the client */
        /* 如果需要的话,生成回复 */
        if (notify) {
            addReply(c,shared.mbulkhdr[3]);
            addReply(c,shared.unsubscribebulk);
            addReplyBulk(c,channel);
            addReplyLongLong(c,dictSize(c->pubsub_channels)+
                           listLength(c->pubsub_patterns));
    
        }
        /* 减少引用计数 */
        decrRefCount(channel); /* it is finally safe to release it */
        return retval;
    }

    订阅/退订pattern的操作也很类似,就不贴代码了。

    它还提供了pubsubUnsubscribeAllChannels和pubsubUnsubscribeAllPatterns,用于一次性退订所有的channels/patters。实现上就是循环调用相应的退订函数。

    发布消息的核心操作

    int pubsubPublishMessage(robj *channel, robj *message) {
        int receivers = 0;
        dictEntry *de;
        listNode *ln;
        listIter li;
    
        /* Send to clients listening for that channel */
        /* 先找到该channel */
        de = dictFind(server.pubsub_channels,channel);
        if (de) {
            list *list = dictGetVal(de);
            listNode *ln;
            listIter li;
    
            listRewind(list,&li);
            /* 对所有订阅该频道的client发送消息 */
            while ((ln = listNext(&li)) != NULL) {
                redisClient *c = ln->value;
    
                addReply(c,shared.mbulkhdr[3]);
                addReply(c,shared.messagebulk);
                addReplyBulk(c,channel);
                addReplyBulk(c,message);
                receivers++;
            }
        }
        /* Send to clients listening to matching channels */
        /* 如果pattern也有client在订阅,那么还要进行模式的匹配并发送消息给相应的client */
        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)) {
                    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++;
                }
            }
            /* 释放getDecodedObject返回的对象 */
            decrRefCount(channel);
        }
        return receivers;
    }

    有了上面的几个核心操作,subscribe/unsubscribe, psubscribe/punsubscribe, publish几个操作基本上就是直接调用上面的函数,这里就不贴代码了。

    有一点提一下:发布/订阅跟具体的db无关,只跟client和具体的channel/pattern有关。

    redis还提供一个pubsub命令,用于输出当前订阅的总体情况:

    void pubsubCommand(redisClient *c) {
        if (!strcasecmp(c->argv[1]->ptr,"channels") &&
            (c->argc == 2 || c->argc ==3))
        {
            /* PUBSUB CHANNELS [<pattern>] */
            /* 如果没有指定pattern,则输出所有channel的信息
             * 如果指定了pattern,则输出跟pattern匹配的所有channel信息
             */
            sds pat = (c->argc == 2) ? NULL : c->argv[2]->ptr;
            dictIterator *di = dictGetIterator(server.pubsub_channels);
            dictEntry *de;
            long mblen = 0;
            void *replylen;
    
            replylen = addDeferredMultiBulkLength(c);
            while((de = dictNext(di)) != NULL) {
                robj *cobj = dictGetKey(de);
                sds channel = cobj->ptr;
    
                if (!pat || stringmatchlen(pat, sdslen(pat),
                                           channel, sdslen(channel),0))
                {
                    addReplyBulk(c,cobj);
                    mblen++;
                }
            }
            dictReleaseIterator(di);
            setDeferredMultiBulkLength(c,replylen,mblen);
        } else if (!strcasecmp(c->argv[1]->ptr,"numsub") && c->argc >= 2) {
            /* PUBSUB NUMSUB [Channel_1 ... Channel_N] */
            int j;
            /* 输出指定channel上client的数量 */
            addReplyMultiBulkLen(c,(c->argc-2)*2);
            for (j = 2; j < c->argc; j++) {
                list *l = dictFetchValue(server.pubsub_channels,c->argv[j]);
    
                addReplyBulk(c,c->argv[j]);
                addReplyLongLong(c,l ? listLength(l) : 0);
            }
        } else if (!strcasecmp(c->argv[1]->ptr,"numpat") && c->argc == 2) {
            /* PUBSUB NUMPAT */
            /* 输出pattern的数量 */
            addReplyLongLong(c,listLength(server.pubsub_patterns));
        } else {
            addReplyErrorFormat(c,
                "Unknown PUBSUB subcommand or wrong number of arguments for '%s'",
                (char*)c->argv[1]->ptr);
        }
    }
  • 相关阅读:
    Javadoc注释的用法
    Java 和 Android系统环境变量设置
    [转载]Android开发新浪微博客户端 完整攻略 [新手必读]
    eclipse中Android程序字符编码不统一的解决方案
    用word2007写blog时表格的显示效果
    解决Vista中的文件关联图标问题。
    用word2007在博客园发布带图片的blog
    在c++中使用gotoxy
    动态多线程任务管理
    几个视频切割工具
  • 原文地址:https://www.cnblogs.com/flypighhblog/p/7763502.html
Copyright © 2011-2022 走看看